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 with transaction_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

FieldNotes
transaction_owner_company_idBrinta id of the selling company.
transaction_owner_company_external_idThe 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

Send exactly one of these:

OptionUse it when
buyer.company_idThe buyer already exists in Brinta.
buyer.company_external_idThe buyer exists in Brinta under your own ID.
buyer.companyYou describe the buyer inline. It requires type (business or person) and address.country.

What matters for tax:

  • type. person makes the sale B2C and business makes it B2B. Many rates apply to only one of the two.
  • address. country is ISO 3166-1 alpha-2. state is ISO 3166-2, for example AR-B or MX-CMX. Brinta uses the most specific level you send (municipality, then state, then country) to find the buyer's jurisdiction.
  • tax_registrations[]. Each entry needs number, type, level (country, state or municipality) and location. The optional status carries the buyer's tax condition, for example Responsable inscripto in Argentina or Lucro real in 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, at buyer level, next to company). 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 with POST /companies and send buyer.company_id. Validations and padrón data stored on the company are then reused automatically. See the Argentina guide.

What is sold: items[]

FieldNotes
amountNet line amount, after discounts and excluding taxes. This is the base Brinta calculates on. Prefer it over final_amount.
final_amountTax-inclusive amount, as an alternative to amount. Brinta backs out the net. Rates with minimum or maximum amounts need amount.
categoriesArray 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_amountInformational. Tax is calculated on amount, not on quantity × unit_amount.

When: invoice_date

invoice_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 PUT does not recalculate taxes.

Other fields that change the result

FieldEffect
payment_method.typeSome rates apply only to a given method: credit card, debit card, prepaid card, digital wallet, bank transfer, cash or intermediaries.
document_typeSome rates apply only to a given document, for example invoice or ticket.
currency, exchange_rateWhen exchange_rate is missing, Brinta looks it up. Minimum and maximum amount thresholds are compared after conversion.

2. Quote or record: type and status

typeWhat happensstatus
calculationBrinta calculates and returns taxes. The result is a quote, not a committed sale.Do not send it.
saleBrinta calculates taxes and records the sale for reports and filings.Required. pending, invoiced, completed, paid or rejected.

Two rules:

  • invoiced requires invoice_date.
  • A calculation cannot carry status, invoice_date or invoice_number. Brinta returns 400 if 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

FieldMeaning
name, typeTax name and family: VAT, GIT (gross income), CIT (income), WHT and others.
rateA fraction: 0.16 means 16%. When rate_type is absolute, rate is a fixed amount instead.
taxable_amountThe base the rate was applied to. It is not always the line amount: some regimes use part of it, or the VAT-inclusive amount.
amountTax amount, rounded to 2 decimals.
level, locationJurisdiction: country, state, city or municipality, with its ISO code (for example AR-C).
withholding_typePERCEPTION or WITHHOLDING when the tax is collected or withheld on behalf of an authority. It is absent for ordinary taxes.
paid_byself, buyer or intermediary.
adds_to_final_amountWhether amount is included in final_amount.
new_regime_previewtrue for taxes not yet in force, shown as a preview (for example Brazil's IBS/CBS).

Totals

  • amount is the sum of the line amounts.
  • final_amount is amount plus every tax with adds_to_final_amount: true. Withholdings configured with a negative rate reduce it.
  • To get the total tax, use final_amount - amount, or add up items[].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 empty taxes array. If you expected tax, check the category, the buyer's address and type, 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": false to store only your taxes. Without it, Brinta adds its own determination alongside yours.
  • If you also send final_amount, it must match amount plus your taxes. Brinta returns 400 if it doesn't.
  • Batches (POST /transaction-batches/) always run with use_tax_engine: false. To have Brinta calculate taxes, send sales one at a time to POST /sales/.

6. Retries and duplicates

  • Retries. Send x-idempotency-key (up to 250 characters, a UUID works) on every POST and PUT. A retry with the same key returns the original response instead of creating a second sale.
  • Duplicate external IDs. transaction_external_id should be unique per seller. By default a second sale with the same external ID is rejected with 400 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

MistakeFix
Sending a seller objectThere 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_dateAdd invoice_date.
state: "Buenos Aires"Use ISO 3166-2: AR-B.
Rates as percentages (21)Rates are fractions: 0.21.
Expecting PUT to recalculateIt 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 ratesSend amount.

Did this page help you?