Record sales and receive payments
A sale that comes in through the API goes down the same path as the sales screen: it gets the company’s number, counts against the plan quota, builds the installments, posts to finance and deducts stock for catalog products. This guide registers a sale in installments and then settles one installment.
The key
Section titled “The key”| Permission | What for |
|---|---|
sales:create | Register the sale |
sales:update | Receive, reverse, reschedule, cancel and create a new version |
sales:read | Read the sale later |
finance:create | Only when the call asks for the finance posting: track_in_finance: true or an account in payment_plan.account_id |
finance:read | To choose the account in payment_plan.account_id, and to see the sale’s account and transactions in the response |
Receiving a payment needs only the sales permission, even when the sale is in finance: the transaction is a consequence of the sale, as on screen.
The member’s access to finance counts too. Sending the track_in_finance field, with true or with false, and choosing the account in payment_plan.account_id require the member the key acts as to have access to finance in the dashboard; without it, the response is 403 member_permission_denied.
The payment plan
Section titled “The payment plan”Every sale carries a payment_plan, and it always has all six fields, even when some are null:
| Field | What it is |
|---|---|
mode | paid_full, entry_and_rest, unpaid or installments |
payment_method | pix, credit_card, debit_card, cash, boleto, transfer, other or null |
account_id | The receiving account, or null for the company’s default account |
paid_at | Date of the upfront payment or of the down payment (YYYY-MM-DD), or null |
entry_amount_cents | Down payment amount, in entry_and_rest mode; null in the others |
installments | The installments, each with amount_cents and due_date; null in paid_full |
The four modes are the ones on screen:
paid_full: paid upfront. The sale starts paid.entry_and_rest: a down payment now and the rest in installments. The down payment must be greater than zero and less than the total, and the installments add up to what is left.installments: in installments, nothing paid yet. The installments add up to the total.unpaid: on credit, to be received. It requiresclient_id, and the installments add up to the total.
The installments must add up to the amount. A difference of up to 1 cent is absorbed by the last installment; more than that returns 422. The total is calculated from the items, the discount, the shipping and the additional costs, the way the form calculates it; you do not send the total.
Register the sale and receive the first installment
Section titled “Register the sale and receive the first installment”# 1. A R$ 1,500.00 sale in 3 boleto installments# Generate the key once and keep it: on a retry, repeat with the same value.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": "Installation of 4 cameras", "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. The first installment was paid. Use the sale id and the installment id# that came back in the response above.SALE_ID="3c5e7a9b-1d3f-4b5d-8f1a-3c5e7a9b1d3f"INSTALLMENT_ID="inst-1"# Generate the key once and keep it: on a retry, repeat with the same value.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;}
// Generate the keys with the operation and store them: on a retry, reuse them.const saleKey = randomUUID();const paymentKey = randomUUID();
// 1. A R$ 1,500.00 sale in 3 boleto installments.const sale = await post( '/sales', { client_id: '0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d', items: [{ type: 'custom', name: 'Installation of 4 cameras', 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. The first installment was paid with 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;}
// Generate the keys with the operation and store them: on a retry, reuse them.$saleKey = bin2hex(random_bytes(16));$paymentKey = bin2hex(random_bytes(16));
// 1. A R$ 1,500.00 sale in 3 boleto installments.$sale = post('/sales', [ 'client_id' => '0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d', 'items' => [ ['type' => 'custom', 'name' => 'Installation of 4 cameras', '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. The first installment was paid with 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']}")
# Generate the keys with the operation and store them: on a retry, reuse them.sale_key = str(uuid.uuid4())payment_key = str(uuid.uuid4())
# 1. A R$ 1,500.00 sale in 3 boleto installments.sale = post( "/sales", { "client_id": "0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d", "items": [ {"type": "custom", "name": "Installation of 4 cameras", "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. The first installment was paid with 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"])The sale, item and response fields are in the Reference. Three points from the example:
- A
customitem is ad hoc and carries the name you send. Acatalogitem points to a registered product (product_id, plusvariation_idwhen the product has variations), and then the name and the promotional price come from the product. - The sale responds with
installments, each one with its ownid. Thatidgoes ininstallment_idwhen receiving, and in rescheduling. payment_statusmoves frompendingtopartialand thenpaidas payments come in, andpaid_centsadds up what has been received.
Receiving
Section titled “Receiving”POST /public/v1/sales/{id}/payments is the Receive dialog on screen:
paid_atis the date the money came in (YYYY-MM-DD), and it is required.- The amount cannot exceed the sale’s balance.
- With
installment_id, the amount settles that installment, which must be open. Without it, the amount is applied in due-date order. account_idchooses the account that received the money, and needsfinance:readandfinance:createon the key, plus the member’s access to finance. Without it, the sale’s account is used.
Every write carries an Idempotency-Key, and for payments it matters more. A payment retried after a network failure, without the key, goes in twice. With the same key, the retry returns the first response and nothing is written again. See Idempotency.
A wrong payment is reversed with POST /public/v1/sales/{id}/payments/{payment_id}/cancel, with the reason in reason. To change amounts and due dates of open installments, use PATCH /public/v1/sales/{id}/installments, which is the Reschedule dialog on screen.
Finance and stock
Section titled “Finance and stock”- Finance. Without the
track_in_financefield, the company’s setting applies, and that automatic posting needs no finance permission. Sending the field, with any value, requires the member to have access to finance;truealso requiresfinance:createon the key, andfalsedoes not. Choosing the account inpayment_plan.account_idrequiresfinance:readandfinance:createon the key, plus the member’s access. - What you see.
account_idandfinance_transactionsonly come in the response when the key hasfinance:readand its member has access to finance. Without that, the sale comes without those two fields, as in the dashboard. Installments and payments always come. - Stock.
decrement_stockdefaults totrue, as on screen, and deducts stock for catalog items without needingproducts:update. Sendfalsewhen stock is managed somewhere else.
Changing and canceling
Section titled “Changing and canceling”A sale is not deleted, not even in the dashboard.
- A commercial change (items, amounts, client, seller, balance plan) is
POST /public/v1/sales/{id}/versions, with the reason inreason. The saleiddoes not change, andversiongoes up. What was already received still counts.GET /public/v1/sales/{id}/versionsreturns the history. - Notes and production change with a
PATCH /public/v1/sales/{id}, without a new version. - Canceling is
POST /public/v1/sales/{id}/cancel, with the reason. It reverses the payments, returns the deducted stock and cancels the transactions.
The locks are the ones on screen, action by action: a sale that is canceled, has an active invoice, has its commission already paid or was generated by a contract refuses some actions with 409. The full table is in the Overview.
When the call fails
Section titled “When the call fails”| Response | What to do |
|---|---|
422 validation_failed | Fix the field listed in errors, such as installments that do not add up to the total |
409 conflict | The sale’s state does not allow it: canceled, active invoice, paid commission or contract |
403 permission_missing | The key is missing a permission, such as finance:create with track_in_finance: true |
403 member_permission_denied | The key’s member has no access to finance and the call touches finance |
429 rate_limit_exceeded | Wait for Retry-After and retry. See Rate limits |
| 5xx or network failure | Retry with the same Idempotency-Key |
Follow it by webhook
Section titled “Follow it by webhook”For your system to know the sale was paid, including when the payment was entered in the dashboard, subscribe an endpoint to sale.payment_registered and sale.paid. Sale events never carry the account or the transactions; those have their own financial_transaction.* events. See the Event catalog.
Next step
Section titled “Next step”- Incremental sync with
updated_after: in the sales list, a new version, a payment and a cancellation count as changes. - Dates and money: converting cents without rounding errors.