Tax determination with /sales
Send a sale to POST /sales/ and Brinta returns every tax that applies to each line, with its rate, base, amount and jurisdiction. You can use the same call to get a quote or to record the sale for your reports and filings.
Tax determination needs four pieces of information:
- Who sells. The seller's location and tax status.
- Who buys. The buyer's location and tax status.
- What is sold. The product or service category of each line, and its amount.
- When. The date that decides which rates are in force.
This guide shows where each of those goes in the request, and how to read what comes back.
Before you start
- The seller is always a company that already exists in Brinta. There is no seller object in the request. Set up the company first with
POST /companies, including its address and tax registrations. Then reference it withtransaction_owner_company_id.- Ask your account manager for the category list that applies to your business, or email [email protected]. The category is the strongest single input to tax determination.
1. Send the request
curl -X POST https://api.brinta.com/sales/ \
-H "Authorization: Bearer $BRINTA_TOKEN" \
-H "Content-Type: application/json" \
-H "x-idempotency-key: 7f1c2b9e-1d5a-4c1e-9a62-0d3c2f1e8b11" \
-d '{
"type": "calculation",
"currency": "MXN",
"transaction_owner_company_id": "2f0e5d75-959b-4791-a201-b88ada67b3f6",
"transaction_external_id": "ORDER-10045",
"payment_method": { "type": "credit card", "name": "Visa" },
"buyer": {
"company": {
"type": "person",
"name": "Client Name",
"legal_name": "Client Name SA",
"email": "[email protected]",
"external_id": "6504cc87cff92e18d366315c",
"address": {
"address_line_1": "Address Line 1234",
"city": "Ciudad de Mexico",
"state": "MX-CMX",
"postal_code": "50121",
"country": "MX"
},
"tax_registrations": [
{ "type": "RFC", "number": "MGI220204FA4", "level": "country", "location": "MX" }
]
}
},
"items": [
{
"item_external_id": "SKU-STREAM-01",
"name": "Streaming Service",
"amount": 1000,
"categories": [{ "code": "43233419" }]
}
]
}'Who sells: the owner
| Field | Notes |
|---|---|
transaction_owner_company_id | Brinta id of the selling company. |
transaction_owner_company_external_id | The seller's ID in your system, as an alternative. Send only one of the two. |
When you send neither, the seller is the company that owns the API token.
Brinta uses the seller company's stored address and tax registrations to match rates. It also uses any registry lists (padrones) the company belongs to.
Who buys: buyer
buyerSend exactly one of these:
| Option | Use it when |
|---|---|
buyer.company_id | The buyer already exists in Brinta. |
buyer.company_external_id | The buyer exists in Brinta under your own ID. |
buyer.company | You describe the buyer inline. It requires type (business or person) and address.country. |
What matters for tax:
type.personmakes the sale B2C andbusinessmakes it B2B. Many rates apply to only one of the two.address.countryis ISO 3166-1 alpha-2.stateis ISO 3166-2, for exampleAR-BorMX-CMX. Brinta uses the most specific level you send (municipality, then state, then country) to find the buyer's jurisdiction.tax_registrations[]. Each entry needsnumber,type,level(country,stateormunicipality) andlocation. The optionalstatuscarries the buyer's tax condition, for exampleResponsable inscriptoin Argentina orLucro realin Brazil. Some rates depend on it. The valid values are in Company Codes. Country and state codes are listed in ISO codes and currencies.physical_address(optional, atbuyerlevel, next tocompany). Use it when the place of supply differs from the buyer's registered address, such as a delivery address. It replaces the buyer's address for tax purposes on this sale only.
If you sell to the same buyers repeatedly, create them once withPOST /companiesand sendbuyer.company_id. Validations and padrón data stored on the company are then reused automatically. See the Argentina guide.
What is sold: items[]
items[]| Field | Notes |
|---|---|
amount | Net line amount, after discounts and excluding taxes. This is the base Brinta calculates on. Prefer it over final_amount. |
final_amount | Tax-inclusive amount, as an alternative to amount. Brinta backs out the net. Rates with minimum or maximum amounts need amount. |
categories | Array of product or service codes. Send one code, [{ "code": "43233419" }], or several codes from different lists, [{ "code": "6491" }, { "code": "117031000", "list": "nbs code" }]. Leave at most one entry without list. If you send no category, the seller's default category is used. |
name, description, item_external_id, quantity, unit_amount | Informational. Tax is calculated on amount, not on quantity × unit_amount. |
When: invoice_date
invoice_dateinvoice_date decides which rates and registry-list memberships are in force. If you leave it out, Brinta uses the current date.
- Send the real document date when you record a past sale.
- Changing the date later with
PUTdoes not recalculate taxes.
Other fields that change the result
| Field | Effect |
|---|---|
payment_method.type | Some rates apply only to a given method: credit card, debit card, prepaid card, digital wallet, bank transfer, cash or intermediaries. |
document_type | Some rates apply only to a given document, for example invoice or ticket. |
currency, exchange_rate | When exchange_rate is missing, Brinta looks it up. Minimum and maximum amount thresholds are compared after conversion. |
2. Quote or record: type and status
type and statustype | What happens | status |
|---|---|---|
calculation | Brinta calculates and returns taxes. The result is a quote, not a committed sale. | Do not send it. |
sale | Brinta calculates taxes and records the sale for reports and filings. | Required. pending, invoiced, completed, paid or rejected. |
Two rules:
invoicedrequiresinvoice_date.- A
calculationcannot carrystatus,invoice_dateorinvoice_number. Brinta returns400if it does.
Converting a quote into a sale. Call PUT /sales/{id} with {"type": "sale", "status": "pending"}. Taxes are kept as they were calculated. They are not recalculated.
3. Read the response
POST /sales/ returns 201 Created. Abridged:
{
"id": "e3e83083-9572-4697-bc19-07c9dafe1cec",
"type": "calculation",
"transaction_external_id": "ORDER-10045",
"transaction_owner_company_id": "2f0e5d75-959b-4791-a201-b88ada67b3f6",
"currency": "MXN",
"exchange_rate": 1,
"amount": 1000,
"final_amount": 1160,
"buyer": { "...": "..." },
"items": [
{
"id": "033cdb01-e9b9-4fa1-b4cd-750e8906d38d",
"item_external_id": "SKU-STREAM-01",
"name": "Streaming Service",
"amount": 1000,
"final_amount": 1160,
"categories": [{ "code": "43233419" }],
"taxes": [
{
"id": "8c0b6f7e-6a3e-4a52-9b6c-6f5a3f7e2a10",
"tax_id": "0f3a5c1e-2b7d-4e8f-9a6b-1c2d3e4f5a6b",
"name": "IVA",
"type": "VAT",
"rate": 0.16,
"rate_type": "percentage",
"taxable_amount": 1000,
"amount": 160,
"level": "country",
"location": "MX",
"paid_by": "self",
"adds_to_final_amount": true
}
]
}
]
}Tax line fields
| Field | Meaning |
|---|---|
name, type | Tax name and family: VAT, GIT (gross income), CIT (income), WHT and others. |
rate | A fraction: 0.16 means 16%. When rate_type is absolute, rate is a fixed amount instead. |
taxable_amount | The base the rate was applied to. It is not always the line amount: some regimes use part of it, or the VAT-inclusive amount. |
amount | Tax amount, rounded to 2 decimals. |
level, location | Jurisdiction: country, state, city or municipality, with its ISO code (for example AR-C). |
withholding_type | PERCEPTION or WITHHOLDING when the tax is collected or withheld on behalf of an authority. It is absent for ordinary taxes. |
paid_by | self, buyer or intermediary. |
adds_to_final_amount | Whether amount is included in final_amount. |
new_regime_preview | true for taxes not yet in force, shown as a preview (for example Brazil's IBS/CBS). |
Totals
amountis the sum of the lineamounts.final_amountisamountplus every tax withadds_to_final_amount: true. Withholdings configured with a negative rate reduce it.- To get the total tax, use
final_amount - amount, or add upitems[].taxes[].amount. The response does not return a separate total-tax field.
No tax is not an error. When no rate matches a line, the line comes back with an emptytaxesarray. If you expected tax, check the category, the buyer's address andtype, and the seller's registrations.
4. Mark the sale as invoiced
Once you issue the invoice, update the sale:
curl -X PUT https://api.brinta.com/sales/e3e83083-9572-4697-bc19-07c9dafe1cec \
-H "Authorization: Bearer $BRINTA_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "type": "sale", "status": "invoiced", "invoice_number": "A-4449529", "invoice_date": "2026-07-10" }'You can also have Brinta issue the e-invoice from the sale with POST /invoices and {"transaction_id": "<sale id>", "document_type": "invoice"}.
To find a sale by your own ID, use GET /sales/ORDER-10045?use_external_id=true. The same query parameter works on PUT.
There is no voided status for sales. To cancel a sale, set it to rejected. To reverse all or part of it after invoicing, create a refund.
5. Supplying your own taxes
If you already calculated taxes, for example on an imported historical sale, send them on each line in items[].taxes[]. Each entry needs at least name and amount.
"taxes": [
{ "name": "IVA", "tax_type": "VAT", "rate": 0.21, "tax_rate_type": "percentage",
"taxable_amount": 1000, "amount": 210, "location": "AR", "tax_level_type": "country",
"adds_to_final_amount": true }
]- Add
"use_tax_engine": falseto store only your taxes. Without it, Brinta adds its own determination alongside yours. - If you also send
final_amount, it must matchamountplus your taxes. Brinta returns400if it doesn't. - Batches (
POST /transaction-batches/) always run withuse_tax_engine: false. To have Brinta calculate taxes, send sales one at a time toPOST /sales/.
6. Retries and duplicates
- Retries. Send
x-idempotency-key(up to 250 characters, a UUID works) on everyPOSTandPUT. A retry with the same key returns the original response instead of creating a second sale. - Duplicate external IDs.
transaction_external_idshould be unique per seller. By default a second sale with the same external ID is rejected with400 Transaction with external ID already exists for this company. Rejected sales do not count. Your account manager can change this behavior for your account.
Common mistakes
| Mistake | Fix |
|---|---|
Sending a seller object | There isn't one. Create the seller with POST /companies and send transaction_owner_company_id. |
Sending status with type: "calculation" | Remove it, or use type: "sale". |
status: "invoiced" without invoice_date | Add invoice_date. |
state: "Buenos Aires" | Use ISO 3166-2: AR-B. |
Rates as percentages (21) | Rates are fractions: 0.21. |
Expecting PUT to recalculate | It doesn't. Send a new sale, or reject this one and create another. |
| Unknown or misspelled fields | /sales rejects unknown top-level fields with 400 Unknown fields: .... |
Using final_amount on lines with threshold-based rates | Send amount. |
Updated about 8 hours ago
