Lançar vendas e receber pagamentos
A venda que entra pela API passa pelo mesmo caminho da tela de vendas: ganha o número da empresa, conta na cota do plano, monta as parcelas, lança no financeiro e baixa o estoque dos produtos do catálogo. Este guia registra uma venda parcelada e depois dá baixa numa parcela.
A chave
Seção intitulada “A chave”| Permissão | Para quê |
|---|---|
sales:create | Registrar a venda |
sales:update | Receber, estornar, reagendar, cancelar e criar versão nova |
sales:read | Ler a venda depois |
finance:create | Só quando a chamada pede o lançamento no financeiro: track_in_finance: true ou uma conta em payment_plan.account_id |
finance:read | Para escolher a conta em payment_plan.account_id, e para ver a conta e os lançamentos da venda na resposta |
Receber um pagamento pede só a permissão de vendas, mesmo com a venda no financeiro: o lançamento é consequência da venda, como na tela.
O acesso do membro ao financeiro também conta. Mandar o campo track_in_finance, com true ou com false, e escolher a conta em payment_plan.account_id exigem que o membro que a chave representa tenha acesso ao financeiro no painel; sem ele, a resposta é 403 member_permission_denied.
O plano de pagamento
Seção intitulada “O plano de pagamento”Toda venda leva um payment_plan, e ele tem sempre os seis campos, mesmo quando alguns são null:
| Campo | O que é |
|---|---|
mode | paid_full, entry_and_rest, unpaid ou installments |
payment_method | pix, credit_card, debit_card, cash, boleto, transfer, other ou null |
account_id | A conta que recebe, ou null para a conta padrão da empresa |
paid_at | Data do pagamento à vista ou da entrada (YYYY-MM-DD), ou null |
entry_amount_cents | Valor da entrada, no modo entry_and_rest; null nos outros |
installments | As parcelas, cada uma com amount_cents e due_date; null no paid_full |
Os quatro modos são os da tela:
paid_full: pago à vista. A venda nasce paga.entry_and_rest: uma entrada paga agora e o resto em parcelas. A entrada precisa ser maior que zero e menor que o total, e as parcelas somam o que sobra.installments: parcelado, nada pago ainda. As parcelas somam o total.unpaid: a receber, no fiado. Exigeclient_id, e as parcelas somam o total.
A soma das parcelas precisa fechar com o valor. Uma diferença de até 1 centavo é ajustada na última parcela; mais que isso responde 422. O total é calculado a partir dos itens, do desconto, do frete e dos custos adicionais, como o formulário calcula; você não manda o total.
Registrar a venda e receber a primeira parcela
Seção intitulada “Registrar a venda e receber a primeira parcela”# 1. Venda de R$ 1.500,00 em 3 parcelas no boleto# Gere a chave uma vez e guarde: numa nova tentativa, repita com o mesmo valor.SALE_KEY=$(uuidgen)curl -X POST "https://api.fatureihoje.com/public/v1/sales" \ -H "Authorization: Bearer $FH_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $SALE_KEY" \ -d '{ "client_id": "0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d", "items": [ { "type": "custom", "name": "Instalação de 4 câmeras", "quantity": 1, "unit_price_cents": 150000 } ], "payment_plan": { "mode": "installments", "payment_method": "boleto", "account_id": null, "paid_at": null, "entry_amount_cents": null, "installments": [ { "amount_cents": 50000, "due_date": "2026-10-10" }, { "amount_cents": 50000, "due_date": "2026-11-10" }, { "amount_cents": 50000, "due_date": "2026-12-10" } ] } }'
# 2. A primeira parcela foi paga. Use o id da venda e o id da parcela# que vieram na resposta acima.SALE_ID="3c5e7a9b-1d3f-4b5d-8f1a-3c5e7a9b1d3f"INSTALLMENT_ID="inst-1"# Gere a chave uma vez e guarde: numa nova tentativa, repita com o mesmo valor.PAYMENT_KEY=$(uuidgen)curl -X POST "https://api.fatureihoje.com/public/v1/sales/$SALE_ID/payments" \ -H "Authorization: Bearer $FH_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $PAYMENT_KEY" \ -d "{ \"amount_cents\": 50000, \"paid_at\": \"2026-10-09\", \"payment_method\": \"pix\", \"installment_id\": \"$INSTALLMENT_ID\" }"import { randomUUID } from 'node:crypto';
const API = 'https://api.fatureihoje.com/public/v1';
async function post(path, body, idempotencyKey) { const res = await fetch(`${API}${path}`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.FH_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey, }, body: JSON.stringify(body), }); const data = await res.json(); if (!res.ok) throw new Error(`${res.status} ${data.error.code}: ${data.error.message}`); return data;}
// Gere as chaves junto com a operação e guarde: numa nova tentativa, reuse.const saleKey = randomUUID();const paymentKey = randomUUID();
// 1. Venda de R$ 1.500,00 em 3 parcelas no boleto.const sale = await post( '/sales', { client_id: '0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d', items: [{ type: 'custom', name: 'Instalação de 4 câmeras', quantity: 1, unit_price_cents: 150000 }], payment_plan: { mode: 'installments', payment_method: 'boleto', account_id: null, paid_at: null, entry_amount_cents: null, installments: [ { amount_cents: 50000, due_date: '2026-10-10' }, { amount_cents: 50000, due_date: '2026-11-10' }, { amount_cents: 50000, due_date: '2026-12-10' }, ], }, }, saleKey,);
// 2. A primeira parcela foi paga no Pix.const firstInstallment = sale.installments[0];const updated = await post( `/sales/${sale.id}/payments`, { amount_cents: firstInstallment.amount_cents, paid_at: '2026-10-09', payment_method: 'pix', installment_id: firstInstallment.id, }, paymentKey,);
console.log(updated.sale_number, updated.payment_status, updated.paid_cents);<?php
const API = 'https://api.fatureihoje.com/public/v1';
function post(string $path, array $body, string $idempotencyKey): array{ $ch = curl_init(API . $path); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('FH_API_KEY'), 'Content-Type: application/json', 'Idempotency-Key: ' . $idempotencyKey, ], CURLOPT_POSTFIELDS => json_encode($body), ]); $data = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($status >= 400) { throw new RuntimeException($status . ' ' . $data['error']['code'] . ': ' . $data['error']['message']); } return $data;}
// Gere as chaves junto com a operação e guarde: numa nova tentativa, reuse.$saleKey = bin2hex(random_bytes(16));$paymentKey = bin2hex(random_bytes(16));
// 1. Venda de R$ 1.500,00 em 3 parcelas no boleto.$sale = post('/sales', [ 'client_id' => '0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d', 'items' => [ ['type' => 'custom', 'name' => 'Instalação de 4 câmeras', 'quantity' => 1, 'unit_price_cents' => 150000], ], 'payment_plan' => [ 'mode' => 'installments', 'payment_method' => 'boleto', 'account_id' => null, 'paid_at' => null, 'entry_amount_cents' => null, 'installments' => [ ['amount_cents' => 50000, 'due_date' => '2026-10-10'], ['amount_cents' => 50000, 'due_date' => '2026-11-10'], ['amount_cents' => 50000, 'due_date' => '2026-12-10'], ], ],], $saleKey);
// 2. A primeira parcela foi paga no Pix.$first = $sale['installments'][0];$updated = post('/sales/' . $sale['id'] . '/payments', [ 'amount_cents' => $first['amount_cents'], 'paid_at' => '2026-10-09', 'payment_method' => 'pix', 'installment_id' => $first['id'],], $paymentKey);
echo $updated['sale_number'], ' ', $updated['payment_status'], ' ', $updated['paid_cents'], PHP_EOL;import jsonimport osimport urllib.errorimport urllib.requestimport uuid
API = "https://api.fatureihoje.com/public/v1"
def post(path, body, idempotency_key): request = urllib.request.Request( API + path, data=json.dumps(body).encode(), method="POST", headers={ "Authorization": f"Bearer {os.environ['FH_API_KEY']}", "Content-Type": "application/json", "Idempotency-Key": idempotency_key, }, ) try: with urllib.request.urlopen(request) as response: return json.load(response) except urllib.error.HTTPError as failure: error = json.load(failure)["error"] raise SystemExit(f"{failure.code} {error['code']}: {error['message']}")
# Gere as chaves junto com a operação e guarde: numa nova tentativa, reuse.sale_key = str(uuid.uuid4())payment_key = str(uuid.uuid4())
# 1. Venda de R$ 1.500,00 em 3 parcelas no boleto.sale = post( "/sales", { "client_id": "0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d", "items": [ {"type": "custom", "name": "Instalação de 4 câmeras", "quantity": 1, "unit_price_cents": 150000} ], "payment_plan": { "mode": "installments", "payment_method": "boleto", "account_id": None, "paid_at": None, "entry_amount_cents": None, "installments": [ {"amount_cents": 50000, "due_date": "2026-10-10"}, {"amount_cents": 50000, "due_date": "2026-11-10"}, {"amount_cents": 50000, "due_date": "2026-12-10"}, ], }, }, sale_key,)
# 2. A primeira parcela foi paga no Pix.first = sale["installments"][0]updated = post( f"/sales/{sale['id']}/payments", { "amount_cents": first["amount_cents"], "paid_at": "2026-10-09", "payment_method": "pix", "installment_id": first["id"], }, payment_key,)
print(updated["sale_number"], updated["payment_status"], updated["paid_cents"])Os campos da venda, do item e da resposta estão na Referência. Três pontos do exemplo:
- O item
customé avulso e leva o nome que você manda. O itemcatalogaponta para um produto do cadastro (product_id, evariation_idquando o produto tem variações), e aí o nome e o preço promocional vêm do cadastro. - A venda responde com
installments, cada parcela com o seuid. É esseidque vai eminstallment_idna hora de receber, e no reagendamento. payment_statusvai dependingparapartiale depoispaidconforme os recebimentos entram, epaid_centssoma o que já foi recebido.
Receber
Seção intitulada “Receber”POST /public/v1/sales/{id}/payments é a janela Receber da tela:
paid_até a data em que o dinheiro entrou (YYYY-MM-DD), e é obrigatório.- O valor não passa do saldo da venda.
- Com
installment_id, o valor quita aquela parcela, que precisa estar em aberto. Sem ele, o valor entra pela ordem de vencimento. account_idescolhe a conta que recebeu, e pedefinance:readefinance:createna chave, além do acesso do membro ao financeiro. Sem ele, vale a conta da venda.
Toda escrita leva Idempotency-Key, e no recebimento isso pesa mais. Um recebimento repetido depois de uma falha de rede, sem a chave, entra duas vezes. Com a mesma chave, a repetição devolve a resposta da primeira e nada é gravado de novo. Veja Idempotência.
Recebimento errado se estorna com POST /public/v1/sales/{id}/payments/{payment_id}/cancel, com o motivo em reason. Para mudar valores e vencimentos das parcelas em aberto, use PATCH /public/v1/sales/{id}/installments, que é a janela Reagendar da tela.
Financeiro e estoque
Seção intitulada “Financeiro e estoque”- Financeiro. Sem o campo
track_in_finance, vale a configuração da empresa, e esse lançamento automático não pede permissão de financeiro. Mandar o campo, com qualquer valor, exige que o membro tenha acesso ao financeiro;trueexige tambémfinance:createna chave, efalsenão. Escolher a conta empayment_plan.account_idexigefinance:readefinance:createna chave, além do acesso do membro. - O que você enxerga.
account_idefinance_transactionssó vêm na resposta quando a chave temfinance:reade o membro dela tem acesso ao financeiro. Sem isso, a venda vem sem esses dois campos, como no painel. Parcelas e recebimentos vêm sempre. - Estoque.
decrement_stocknascetrue, como na tela, e baixa o estoque dos itens do catálogo sem pedirproducts:update. Mandefalsequando o estoque é controlado em outro lugar.
Mudar e cancelar
Seção intitulada “Mudar e cancelar”Venda não se exclui, nem no painel.
- Mudança comercial (itens, valores, cliente, vendedor, plano do saldo) é
POST /public/v1/sales/{id}/versions, com o motivo emreason. Oidda venda não muda, eversionsobe. O que já foi recebido continua valendo.GET /public/v1/sales/{id}/versionstraz o histórico. - Observações e produção mudam num
PATCH /public/v1/sales/{id}, sem versão nova. - Cancelar é
POST /public/v1/sales/{id}/cancel, com o motivo. Estorna os recebimentos, devolve o estoque baixado e cancela os lançamentos.
As travas são as da tela, ação por ação: venda cancelada, com nota fiscal ativa, com comissão já paga ou gerada por contrato recusa algumas ações com 409. A tabela completa está na Visão geral.
Quando a chamada falha
Seção intitulada “Quando a chamada falha”| Resposta | O que fazer |
|---|---|
422 validation_failed | Corrija o campo apontado em errors, como parcelas que não fecham com o total |
409 conflict | A situação da venda não deixa: cancelada, com nota ativa, com comissão paga ou de contrato |
403 permission_missing | Falta permissão na chave, como finance:create com track_in_finance: true |
403 member_permission_denied | O membro da chave não tem acesso ao financeiro e a chamada mexe no financeiro |
429 rate_limit_exceeded | Espere o Retry-After e repita. Veja Limites de uso |
| 5xx ou falha de rede | Repita com a mesma Idempotency-Key |
Acompanhar pelo webhook
Seção intitulada “Acompanhar pelo webhook”Para o seu sistema saber que a venda foi paga, inclusive quando o pagamento foi lançado no painel, inscreva um endpoint em sale.payment_registered e sale.paid. Os eventos de venda nunca levam a conta nem os lançamentos; esses têm os eventos financial_transaction.*. Veja o Catálogo de eventos.
Próximo passo
Seção intitulada “Próximo passo”- Sincronização incremental com
updated_after: na lista de vendas, uma versão nova, um recebimento e um cancelamento contam como alteração. - Datas e dinheiro: converter centavos sem erro de arredondamento.