Use case

Paying Brazilian sellers and freelancers: the document layer

You cannot issue your sellers' notas fiscais, and you should not try. What you can do — and what mature platforms do — is a payout statement good enough that their accountant stops emailing you.

Updated 2026-08-07 · 8 min read · by the FaturaPDF team

Any platform that moves money to Brazilian sellers, drivers, creators or freelancers hits the same sequence: payouts work, then sellers start asking for "a nota fiscal" or "a document for my accountant", and someone has to decide what the platform actually owes them.

The answer is narrower than it looks, and knowing where the boundary is saves a quarter of engineering. The underlying distinction — which documents a tax authority authorizes and which are private — is covered in the document taxonomy.

What you cannot do

You cannot issue the seller's nota fiscal

A nota fiscal is issued by the party providing the goods or service, using their CNPJ and their ICP-Brasil digital certificate, through a state (NF-e) or municipal (NFS-e) system. If the provider is your user, the obligation is theirs and the credentials are theirs. Holding sellers' certificates so you can issue on their behalf is technically conceivable and a liability most platforms correctly decline.

The corollary is a useful thing to put in your help centre: the platform documents the payment it made; the seller documents the sale they made. Two different documents, two different issuers.

What you should do

  1. Issue a payout statement or recibo for every transfer: what was paid, for which orders or period, gross, deductions, net, when, and to whom.
  2. Expose the data the seller needs to issue their own document — buyer, amount, date, description — in a form they can copy, and ideally as a CSV or API.
  3. Store their tax ID correctly, validated at onboarding. This is the item that causes the most pain when skipped.
  4. Optionally, track their compliance: a field for the nota fiscal number they issued, so your finance team can see coverage without chasing.

Validate tax IDs at onboarding, not at payout

The expensive version of this bug: a seller signs up with a mistyped CNPJ, trades for eight months, and the error surfaces the first time you generate a document — at month end, in a batch, for someone who may no longer answer email.

Check digits are a pure local function; there is no reason not to run them at signup. See the validation use case for the storage and masking decisions, and the algorithm reference for the maths.

Store digits, index on digits

Persist "11222333000181", not the masked form, with a unique index on the normalized column. Otherwise the same seller registers twice in two formats and your payout aggregation quietly splits in half.

Modelling the statement

A payout is an aggregate. Keep the aggregation in your domain and the document as a derived artefact:

typescriptdomain.ts
interface Payout {
  id: string;
  sellerId: string;
  periodStart: Date;
  periodEnd: Date;
  grossCents: number;      // sum of the seller's share of order lines
  feeCents: number;        // your commission
  adjustmentCents: number; // refunds, chargebacks, corrections
  netCents: number;        // what actually left your account
  paidAt: Date;
  method: "PIX" | "TED";
  documentPath?: string;   // the generated PDF, once produced
}

// Invariant worth asserting before you generate anything:
//   gross - fee + adjustment === net
// A statement that does not add up is worse than no statement.

That invariant is not pedantry. The single most common complaint about payout statements is that the numbers do not reconcile with the bank transfer, and it is almost always an adjustment applied in one place and not the other.

Generating them at month end

javascriptpayout-payload.js
const HOST = "brazilian-invoice-receipt-pdf-api-cpf-cnpj.p.rapidapi.com";

function payoutPayload(payout, seller, company) {
  const brl = (cents) => cents / 100;

  return {
    tipo: "recibo",
    numero: `PAY-${payout.id}`,
    data: payout.paidAt.toISOString().slice(0, 10),
    emitente: {
      nome: seller.legalName,
      documento: seller.taxIdDigits,     // validated at onboarding
    },
    destinatario: {
      nome: company.legalName,
      documento: company.cnpj,
    },
    itens: [
      {
        descricao: `Repasse de vendas - ${fmtPeriod(payout.periodStart, payout.periodEnd)}`,
        valor_unitario: brl(payout.grossCents),
      },
    ],
    // The API subtracts this from the subtotal, so the printed total is the net.
    desconto: brl(payout.feeCents - payout.adjustmentCents),
    forma_pagamento: payout.method,
    observacoes:
      `Bruto: ${fmtBRL(brl(payout.grossCents))} | ` +
      `Comissao da plataforma: ${fmtBRL(brl(payout.feeCents))} | ` +
      `Ajustes: ${fmtBRL(brl(payout.adjustmentCents))} | ` +
      `Liquido transferido: ${fmtBRL(brl(payout.netCents))}. ` +
      `Este documento comprova o repasse efetuado pela plataforma e nao substitui ` +
      `a nota fiscal de responsabilidade do vendedor.`,
  };
}
Note who is emitente

On a payout receipt the seller is the issuer — they are acknowledging receipt of the money — and the platform is the counterparty. That is the reverse of a sales invoice and it is easy to get backwards, which produces a document that reads as if the platform received the payment.

Also note the last sentence in observacoes. Stating explicitly that the statement does not replace the seller's nota fiscal costs one line and prevents a recurring misunderstanding.

Running the batch

javascriptbatch.js
async function generateCycleDocuments(payouts, { concurrency = 6 } = {}) {
  const results = { ok: 0, failed: [] };
  const queue = [...payouts];

  await Promise.all(
    Array.from({ length: concurrency }, async () => {
      while (queue.length) {
        const payout = queue.shift();
        try {
          const seller = await sellers.find(payout.sellerId);
          const pdf = await generateWithRetry(payoutPayload(payout, seller, COMPANY));
          await storage.put(`payouts/${payout.id}.pdf`, pdf);
          await payouts_.markDocumented(payout.id);
          results.ok++;
        } catch (err) {
          // A 400 here means bad seller data - it needs a human, not a retry.
          results.failed.push({ payoutId: payout.id, reason: err.message });
        }
      }
    })
  );

  if (results.failed.length) await alertFinance(results.failed);
  return results;
}

Two operational details that matter more than the code: keep concurrency modest so a quota limit does not abort the run halfway, and treat validation failures as a data alert routed to whoever can contact the seller — not as an error swallowed into a log.

What sellers actually ask for

RequestWhat to give them
"Send me the nota fiscal"You cannot — explain that they issue it. Give them the payout statement plus the buyer/amount/date data.
"My accountant needs the breakdown"Gross, commission, adjustments, net, per cycle. Ideally downloadable as CSV as well as PDF.
"How much did I earn in 2026?"An annual summary. Cheap to build from the same aggregate, and it removes a support ticket per seller per year.
"The amount does not match my bank"Almost always an adjustment shown in one place and not the other. Assert the invariant.
"I need it in my company's name, not mine"Let them switch between CPF and CNPJ on their profile, and regenerate future statements. Do not silently rewrite past ones.

Scope, stated plainly

This page is about the document layer. Moving the money — PIX or TED transfers, split payments, escrow, tax withholding — is your payment provider's territory and is regulated. And whether your platform has withholding obligations for payments to individuals is a question for a Brazilian accountant, not for a PDF generator. What is safe to say: whatever you withhold, show it on the statement. An unexplained difference between gross and net is the fastest way to lose a seller's trust.

Frequently asked questions

Can my marketplace issue notas fiscais for its sellers?

No. The nota fiscal must be issued by the service or goods provider using their own digital certificate and their own registration. Platforms issue a payout statement or recibo for the transfer they made, and give sellers the data they need to issue their own document.

What document should a platform give a freelancer?

A payout statement or recibo covering the payment: period, gross, fees, adjustments, net, date and method, with both parties' tax IDs. Add a line clarifying that it does not replace the freelancer's own fiscal document — it prevents a recurring misunderstanding.

Do I need to withhold taxes on payments to Brazilian individuals?

Possibly, depending on the nature of the payment, the payer and the payee's status. That is a question for a Brazilian accountant, not something a document API can answer. Whatever the answer, itemize any withholding on the statement rather than leaving an unexplained gap between gross and net.

How do I handle a seller who registered with a CPF but now has a CNPJ?

Let them update the profile and use the new identifier for future statements. Do not retroactively rewrite documents already issued — those record what was true at the time, and reissuing them silently is exactly the kind of thing that fails an audit.

Generate payout statements automatically

One POST per seller per cycle produces a recibo-style PDF with the seller's validated CPF/CNPJ, the gross, the fees and the net in BRL. At month end it is a loop, not a project.

Get an API key on RapidAPI → Or try the free browser generator

Related guides