Integração · API v1

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

API v1

Comece em 3 passos

REST sobre HTTPS, JSON na entrada e na saída. Toda resposta traz um campo object identificando o recurso.

Hoje o Obsyd processa PIX. Só PIX.

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.

1Crie uma chave

Em API keys, gere uma sk_test_ para homologar. O segredo aparece uma única vez — guarde em variável de ambiente.

2Crie a cobrança

POST /charges devolve o código PIX, o QR e uma checkout_url pronta para receber o cliente.

3Confirme o pagamento

Assine o webhook charge.paid e libere o pedido por ele. O redirect do cliente não é prova de pagamento.

Base URL
https://app.obsydpagamentos.com/api/v1

Todos os caminhos desta página são relativos a ela.

Cabeçalho de autenticação
Authorization: Bearer sk_live_SUA_CHAVE

Chaves em Painel → API keys. sk_test_ usa o sandbox.

Prefere que a IA faça por você?

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.

Montar meu prompt
Migrando

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.

TemaComo costuma serComo é aqui
ValorOra 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 descontoManda 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.
metadataVolta na consulta, mas some no webhook.Ecoado no webhook, exatamente como você mandou — junto com external_id e track.
WebhookSem assinatura, ou “confie no IP de origem”.HMAC-SHA256 sobre <t>.<corpo cru>, com timestamp para barrar replay.
Entrega falhouVocê descobre pelo cliente reclamando.Log de entrega no painel, com status HTTP, erro, tentativas e botão de reenviar.
Tempo realPolling é 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.
CredencialUma chave, acesso total.Escopos por chave e allowlist de IP por chave, valendo em todas as rotas daquela credencial.
SaqueIdempotê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.
Chaves

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 devolve 403 forbidden.
  • A chave isola os ambientes: uma chave live nunca enxerga cobrança test (e vice-versa), inclusive na listagem.
  • Cada chamada atualiza o last_used_at da chave — dá para auditar o uso em Painel → API keys.
RotaLimite por chaveEscopo exigido
POST /v1/charges · GET /v1/charges60 req/mintransactions
GET /v1/charges/:id120 req/mintransactions
GET /v1/events30 aberturas/mintransactionsmáx. 5 conexões simultâneas por conta
GET /v1/balance120 req/mindata
POST /v1/withdrawals20 req/minwithdrawals
GET /v1/withdrawals · GET /v1/withdrawals/:id120 req/minwithdrawals ou data
  • Ao estourar o limite: 429 rate_limited com Retry-After em 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.
Permissões

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.

EscopoO que concedeRotas cobertas
transactionsCriar, 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
withdrawalsPedir 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
dataLer saldo e consultar saques. Não cria e não move nada.GET /v1/balance · GET /v1/withdrawals (leitura)
checkoutReservado 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 transactions e 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 withdrawals ou data: quem só lê relatório não precisa de permissão para tirar dinheiro.
Sem o escopo403 · code estável
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"]
Permissões

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 regra 203.0.113.7 continua 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 de x-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.
Formatos aceitosuma regra por linha
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 comparar
IP fora da lista403 · code estável
HTTP/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" }
  }
}
Se a chave tem allowlist e a plataforma não informou nenhum IP de origem, a chamada também é recusada (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.
Confiabilidade

Idempotência

Rede falha no meio. O header Idempotency-Key existe para que repetir a mesma chamada não crie o recurso duas vezes.

Fluxocobrança e saque
# 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
Em cobrança — opcional, mas use
  • 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 id e 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.
Em saque — obrigatória
  • 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 POST simultâneos com a mesma chave não produzem dois saques.
  • Mesma chave com valor diferente devolve 409 idempotency_key_reused, com o saque existente em details.
Um 504 num saque NÃO significa que o saque falhou. A resposta pode ter se perdido depois de o dinheiro já ter saído. Sem chave de idempotência, quem repete o 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”.
Sandbox

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/balance com chave de teste responde zerado, com um aviso no campo note.
  • Saque não existe em teste: POST /v1/withdrawals com sk_test_ responde 400 test_mode_unsupported.
  • O campo mode aparece 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.
GET /v1/balancechave sk_test_
{
  "object": "balance",
  "mode": "test",
  "available_cents": 0,
  "pending_cents": 0,
  "currency": "BRL",
  "note": "Cobranças de teste não movimentam saldo."
}
POST/api/v1/charges

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

60 req/mintransactionsIdempotente
Corpoapplication/json
CampoTipoDescrição
amount_centsobrigatóriointegerTotal 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_centsinteger ≥ 0Frete, em centavos.
extra_fee_centsinteger ≥ 0Taxa extra (conveniência, embalagem, serviço).
discount_centsinteger ≥ 0Desconto, em centavos.
descriptionstring (≤ 200)Aparece na página de pagamento e no extrato do painel.
customerobjectname (≤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_idstring (≤ 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}.
metadataobjectChaves livres, devolvidas como enviadas — inclusive no webhook. return_url (http/https) vira o botão “voltar para a loja” depois do pagamento.
expires_in_secinteger (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).
trackobjectUTMs e click ids do comprador. Ver Rastreamento.
client_ipstring (≤ 60)IP do comprador — não o do seu servidor. Omitido, cai para o IP de quem chamou a API.
client_uastring (≤ 300)User-agent do comprador. Omitido, cai para o User-Agent da requisição.
Cabeçalhos
CampoTipoDescrição
AuthorizationobrigatóriostringBearer sk_live_… ou Bearer sk_test_… (ou o cabeçalho X-Api-Key).
Content-Typeobrigatóriostringapplication/json
Idempotency-Keystring (≤ 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-Trackstring (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) e pix.qr_url (PNG). qr_base64 quase nunca vem — e, quando não vem, a chave simplesmente não existe.
  • fee_cents e net_cents já 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/503 com acquirer_unavailable e a cobrança fica como failed. Ao repetir, use uma Idempotency-Key nova — a anterior já está amarrada à cobrança que falhou.
Cobranças

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.

amount_cents = Σ(item.quantity × item.unit_price_cents)
+ shipping_fee_cents + extra_fee_cents − discount_cents
  • Com items[]: o total é derivado. Você pode omitir amount_cents e 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 em items_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ão amount_mismatch), com details.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.
Quando a soma não fecha
400 amount_mismatchcode estável
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_mismatch também aparece sem itens, quando frete + taxa − desconto passa do amount_cents informado — o sintoma clássico de quem mandou o subtotal achando que era o total. Aí expected_cents é o total dos acessórios.
  • Trate amount_mismatch como 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.
Cobranças

Divisão do recebimento (splits)

Campo opcional splits[] no POST /charges — até 20 recebedores, em valor fixo ou percentual, misturáveis no mesmo array.

ChaveRegra
merchant_id ou merchant_slugObrigatório, exatamente um dos dois. merchant_id precisa ser uuid; o slug é normalizado (trim + minúsculo).
amount_cents ou percentObrigatório, exatamente um por item.
amount_centsInteiro em CENTAVOS, maior que zero.
percentNú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.
descriptionOpcional, 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 isso percent: 100 nã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 slug contra id do mesmo merchant).
  • details.index e details.field sempre 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 um split_out negativo na conta vendedora e um split_in positivo 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 receber charge.paid.
Cobranças

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 Referer da requisição, o header X-Obsyd-Track com um JSON, e o campo track no corpo.
  • Chaves aceitas: qualquer utm_* (utm_ + até 24 caracteres) e mais fbclid, 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 charge e, portanto, no webhook que você recebe: a atribuição fecha sem você guardar nada do seu lado. Sem nenhuma chave capturada, track simplesmente não aparece.
Modelo

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.

CampoTipoDescrição
idstring (uuid)Identificador da cobrança no Obsyd.
object"charge"Discriminador do recurso.
statusenumpending · paid · expired · refunded · failed.
mode"live" | "test"Ambiente da chave que criou a cobrança.
method"pix"Único método disponível hoje.
amount_centsintegerTotal cobrado, em centavos.
fee_centsintegerTaxa do Obsyd sobre esta cobrança.
net_centsintegerLíquido creditado no saldo quando paga.
currencystringSempre BRL.
itemsopcionalarraySó quando você mandou itens; cada item ganha total_cents.
items_total_centsopcionalintegerSubtotal. Só quando há composição.
shipping_fee_centsopcionalintegerFrete. Só quando há composição.
extra_fee_centsopcionalintegerTaxa extra. Só quando há composição.
discount_centsopcionalintegerDesconto. Só quando há composição.
descriptionopcionalstringDescrição enviada na criação.
external_idopcionalstringSeu id de pedido.
customeropcionalobjectExatamente o que você enviou.
metadataopcionalobjectExatamente o que você enviou — inclusive no webhook.
trackopcionalobjectUTMs e click ids capturados na criação.
client_ipopcionalstringIP do comprador, capturado na criação.
client_uaopcionalstringUser-agent do comprador.
pixopcionalobjectcode (copia-e-cola), qr_base64 e qr_url (PNG público).
splitsopcionalarraySó na resposta do POST /charges, e só quando há divisão.
checkout_urlstring (url)Página de pagamento hospedada.
expires_atopcionalstringValidade do PIX, ISO 8601.
paid_atopcionalstringMomento da confirmação.
refunded_atopcionalstringMomento do estorno.
failed_reasonopcionalstringMotivo quando status = failed.
created_atstringCriação, ISO 8601 — também serve de cursor na listagem.
updated_atstringÚltima alteração, ISO 8601.
eventsopcionalarraySomente em GET /v1/charges/:id.
Os campos marcados opcional somem da resposta quando não têm valor — não vêm como null. Programe com "paid_at" in charge ou optional chaining, nunca comparando com null.
GET/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.

120 req/mintransactionsinclui events[]
Rota
CampoTipoDescrição
idobrigatóriostring (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.
Objeto eventitens de events[], do mais antigo para o mais novo
CampoTipoDescrição
typestringcreated, status_changed, routing_failover ou ignored_transition.
fromstringStatus anterior. Omitido quando não há.
tostringStatus resultante. Omitido quando não há.
sourcestringQuem provocou: api, webhook, poll, admin, sandbox, cron ou system.
atstringData/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"
GET/api/v1/charges

Listar cobranças

Ordenado por created_at decrescente, com paginação por cursor. Só devolve cobranças do ambiente da chave usada.

60 req/mintransactionspaginado
Query string
CampoTipoDescrição
statusenumpending, paid, expired, refunded ou failed.
fromstring (ISO 8601)created_at maior ou igual. Data e hora completas com fuso, ex.: 2026-08-01T00:00:00Z.
tostring (ISO 8601)created_at menor ou igual, mesmo formato.
external_idstringFiltra pelo seu id de pedido (igualdade exata).
limitinteger (1–100)Itens por página.padrão 50
cursorstring (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_more indica se existe página seguinte; next_cursor é o created_at do último item devolvido.
  • Para conciliação diária, combine status=paid com from/to e some net_cents — não amount_cents.
Recursos públicos

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_url aponta 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 devolve 404.
  • Existe também GET /api/public/charges/{id}/status, sem autenticação, com apenas id, status, paid_at e expires_at — é o que a página de pagamento usa. Serve para acompanhar a tela do cliente, nunca para liberar o pedido.
HTML
<!-- 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>
Status públicosem chave · sem cache
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" }
Obsyd → você

Webhooks

Cadastre a URL em Painel → Webhooks e escolha os eventos. Cada endpoint recebe um segredo próprio, usado para assinar todas as entregas.

EventoQuando dispara
charge.paidPIX confirmado. net_cents vai para o saldo. É o evento que deve liberar o pedido.
charge.expiredPassou de expires_at sem pagamento.
charge.refundedEstorno concluído; o valor sai do saldo.
charge.failedNão foi possível gerar ou cobrar o PIX; veja failed_reason.
test.pingEnviado pelo botão “enviar teste” do painel. Aqui data não é uma cobrança.
Entregao que chega na sua URL
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çalhoConteúdo
X-Obsyd-EventTipo do evento, igual ao campo type do corpo.
X-Obsyd-Delivery-IdId da entrega (uuid). Use como chave de idempotência — a mesma entrega pode chegar duas vezes.
X-Obsyd-Signaturet=<unix>,v1=<hmac-sha256 hex>, calculado sobre <t>.<corpo cru>.
User-AgentObsyd-Webhooks/1.0
Verificar a assinatura
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 2xx em até 8 segundos. Depois disso a conexão é abortada e a entrega vira retentativa.
  • Redirecionamentos não são seguidos: um 3xx conta 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.expired de uma cobrança que depois foi paga pode chegar depois do charge.paid. Estado final não volta atrás.
  • Um mesmo endpoint recebe eventos de teste e de produção — separe pelo data.mode quando precisar.
  • Em caso de dúvida sobre um pagamento, GET /v1/charges/{id} é a fonte da verdade.
GET/api/v1/events

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

30 aberturas/mintransactions5 conexões por conta
Cabeçalhos
CampoTipoDescrição
AuthorizationobrigatóriostringBearer sk_live_…. O stream é filtrado por conta e por ambiente: sk_test_ só vê cobrança de teste.
Acceptstringtext/event-stream
Last-Event-IDstring<updated_at>|<charge_id> — o id do último evento processado. Retoma exatamente do ponto seguinte. Também aceito como ?last_event_id=.
Query string
CampoTipoDescrição
eventsstring (csv)Filtra os tipos, ex.: charge.paid,charge.refunded. Omitido, vêm os quatro.
sincestring (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"
QuadroPara que serve
hidefy.readyAbre 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.expired
charge.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.
: pingComentário a cada 15 s, para o proxy não derrubar a conexão ociosa.
hidefy.reconnect
hidefy.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 quadro ready avisa resumed: false.
  • Persista o id depois 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_limited com details.reason: "too_many_connections". Um stream por processo é o suficiente.
  • EventSource de navegador não manda Authorization — e a chave secreta nunca deveria ir para o navegador. Consuma o SSE do seu servidor.
GET/api/v1/balance

Consultar saldo

Saldo da conta em centavos. Disponível é o que pode ser sacado agora; pendente ainda não liquidou.

120 req/mindata
curl https://app.obsydpagamentos.com/api/v1/balance \
  -H "Authorization: Bearer sk_live_SUA_CHAVE"
  • Esta rota exige o escopo data — uma chave só de transactions recebe 403 insufficient_scope.
  • Saques em análise já saem de available_cents no momento do pedido — não há risco de contar duas vezes.
  • Com chave sk_test_ a resposta é sempre zerada, com o campo note explicando.
POST/api/v1/withdrawals

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

20 req/minwithdrawalsIdempotency-Key obrigatória
Corpoapplication/json
CampoTipoDescrição
amount_centsobrigatóriointeger > 0Valor em centavos, acima do mínimo configurado. Abaixo dele: 400 below_minimum com details.min_cents.
pix_keystring (≤ 140)Destino declarado. É conferência, não escolha: precisa bater com a chave cadastrada na conta, senão 400 pix_key_mismatch.
pix_key_typeenumcpf, cnpj, email, phone ou evp. Obrigatório junto com pix_key — mandar um sem o outro é invalid_request.
external_idstring (≤ 120)Seu id de referência. Filtra a listagem via ?external_id=.
Cabeçalhos
CampoTipoDescrição
Idempotency-Keyobrigatóriostring (≤ 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"
  }'
O saque só vai para a chave PIX cadastrada na conta. Se a API pudesse escolher o destino, um vazamento de credencial viraria saque para conta de terceiro. Para sacar para outro destino, troque a chave em Configurações — não pelo corpo da requisição.
Campo da respostaSignificado
amount_centsO valor pedido, em centavos.
fee_centsTaxa de saque, quando houver.
total_debit_centsamount_cents + fee_cents — o que realmente sai do saldo. É esse número que precisa caber em available_cents.
statusrequestedapprovedpaid, ou rejected (o valor volta para o saldo) / cancelled.
reviewed_at · paid_atVê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_urlObservação da análise e comprovante. Omitidos enquanto não existem.
  • Pré-condições: conta ativa (senão 403 merchant_inactive), KYC aprovado (senão 403 kyc_required) e chave PIX cadastrada (senão 400 pix_key_missing).
  • Em modo de teste a rota responde 400 test_mode_unsupported: saque não é simulável.
  • A dedupe da Idempotency-Key acontece dentro da transação que debita o ledger — dois POST simultâneos com a mesma chave não geram dois saques.
GET/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.

120 req/minwithdrawals ou data
Query stringsó na listagem
CampoTipoDescrição
statusenumrequested, approved, paid, rejected ou cancelled.
external_idstringIgualdade exata.
fromstring (ISO 8601)created_at maior ou igual.
tostring (ISO 8601)created_at menor ou igual.
limitinteger (1–100)Itens por página.padrão 50
cursorstring (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 — nunca 403, 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.
Ciclo de vida

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.

StatusSignificadoPode virar
AguardandoCobrança criada, código PIX válido até expires_at.paid · expired · failed
PagoPIX confirmado. paid_at preenchido e net_cents creditado no saldo.refunded
ExpiradoPassou da validade sem pagamento.paid (webhook atrasado ainda credita)
EstornadoEstorno concluído; o valor sai do saldo. refunded_at preenchido.— estado final
FalhouNão foi possível gerar o PIX; o motivo fica em failed_reason.paid (raro: adquirente confirma depois)
Nunca libere o pedido pelo retorno do cliente ao seu site — ele pode fechar a aba, voltar sem pagar ou pagar e não voltar. Espere o evento charge.paid, consulte GET /v1/charges/{id} ou escute o stream. E lembre: estado final não volta atrás.
Referência

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.

Formatoqualquer status ≥ 400
{
  "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ódigos gerais
CódigoHTTPQuando acontece / o que fazer
invalid_request400Campo 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_mismatch400A 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_split400Divisão inválida. details.index/details.field apontam o item e details.reason diz o motivo.
method_not_supported400Você mandou method diferente de pix ou um campo de cartão. details.supported lista o que existe.
unauthorized401Chave ausente, malformada ou revogada. Confira o cabeçalho e a chave.
forbidden403Conta bloqueada ou fora de operação. Fale com o suporte.
insufficient_scope403A credencial não tem o escopo da rota. details.required_scope (e/ou required_scopes) diz exatamente o que marcar no painel.
ip_not_allowed403O IP de origem não está na allowlist da chave. details.ip traz o IP que vimos.
not_found404Recurso inexistente, de outro merchant ou do outro ambiente (test × live).
conflict409Estado incompatível com a operação pedida.
rate_limited429Limite da chave estourado. Espere os segundos indicados em Retry-After e repita. No SSE também cobre details.reason: "too_many_connections".
acquirer_unavailable502 / 503Nenhuma adquirente conseguiu gerar o PIX; a cobrança fica failed. Repita com uma Idempotency-Key nova.
internal500Erro nosso. Repita; se persistir, chame o suporte com o horário da chamada.
Códigos exclusivos de saque
CódigoHTTPSignificado
idempotency_key_required400Faltou o header Idempotency-Key. Ele é obrigatório no saque, e o motivo está acima.
idempotency_key_reused409A mesma chave já foi usada com outro valor. details.existing_withdrawal_id e details.existing_amount_cents.
insufficient_balance400details: requested_cents, fee_cents e total_debit_cents.
below_minimum400Abaixo do saque mínimo. details.min_cents.
pix_key_missing400A conta não tem chave PIX cadastrada.
pix_key_mismatch400O destino informado não bate com a chave cadastrada.
merchant_inactive403Conta não está ativa.
kyc_required403Saques liberam após a aprovação da conta.
test_mode_unsupported400Saque não existe em modo de teste. Use uma chave sk_live_.
  • Repita: 5xx e 429, com backoff. No saque, sempre com a mesma Idempotency-Key.
  • Não repita: 4xx de 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.
Ficou faltando alguma coisa?

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.

Integrar com IA