Batches
Invoices and retention certificates from one CSV file, up to 100 million rows.
A batch does from a CSV what the JSON endpoints do one document at a time: stamp invoices or retention certificates. Use it when you send thousands or millions of documents, such as a monthly cycle of certificates for your workers.
How it works
POST /batcheswith thetypeof the rows and, if you want,defaults. It answers withupload_urlandupload_expires_at, 24 hours later. Only this call returnsupload_url, because the URL is a write credential:GET /batches/{id}always answers it asnull. If you lost it, repeatPOST /batcheswith the sameIdempotency-Key.PUTthe CSV toupload_url, plain or compressed with gzip. It needs no token: the URL itself authorizes the upload. For a large file, upload it in parts. heru checks the format and the catalogs of every row, not the receiver against the SAT, and the batch goesvalidating.GET /batches/{id}until it isready,donewhen every document is a duplicate, orinvalidwhen no document is valid and at least one failed validation. Areadybatch shows what can and what cannot be processed, before anything is stamped:valid_documentsandinvalid_documents(andvalid_rowsandinvalid_rows), the first 100 problems inerrorswith the row, the column and a code, all of them inerrors_url, and the estimate of when it would finish. Only rows are rejected, never the whole file because of a bad row: the valid documents go ahead, and you can fix the rest in a new batch.POST /batches/{id}/processstamps the valid documents. Iffits_lifetimeisfalseit answers409 BATCH_EXCEEDS_CAPACITYand starts nothing, unless you ask forallow_partial. The batch goesprocessing, thendone. Each stamped document sends its own event (invoice.stampedorretention_certificate.stamped) as soon as it is stamped, andbatch.donefires when every valid document has its outcome. You can pause, resume or cancel it on the way.- Read
report_urlfor the result of every row. Thereport_url(anderrors_url) inside thebatch.doneevent is a signed link valid for 15 minutes from when the event was written: if you receive the event later than that, or a retry arrives later, callGET /batches/{id}for a fresh link.
Uploading never stamps anything: only process does.
A credential that covers several of your companies and does not name one answers 422 COMPANY_REQUIRED to POST /batches, GET /batches/{id} and POST /batches/{id}/process. See Companies.
type | Each row is | Scope besides batches:write |
|---|---|---|
invoice | One item of an invoice | invoices:write |
retention_certificate | One service of a retention certificate | retention_certificates:write |
The file
UTF-8, comma separated with RFC 4180 quoting, and a header row. Column names are case-insensitive and their order does not matter; columns that are not listed are ignored. Each column takes the same values as the field of the JSON endpoint it comes from, and a few exist only in batches, marked as such in the tables below. Columns marked * are required, in the row or in defaults.
Rows that share external_id are one document, and they must be together. Put them one after the other: if the same external_id appears again further down the file, all its rows are invalid with EXTERNAL_ID_NOT_CONTIGUOUS. Their document columns must match on every row (DOCUMENT_MISMATCH). If any row of a document does not pass validation, the whole document is not stamped.
Limits
| Limit | Value | Over the limit |
|---|---|---|
| Data rows per file | 100,000,000 | The batch goes invalid with TOO_MANY_ROWS |
| File size, uncompressed | 32 GiB | The batch goes invalid with FILE_TOO_LARGE |
| File size as uploaded | 32 GiB, in upload_max_bytes | The upload is refused |
| Columns | 256 | The batch goes invalid with TOO_MANY_COLUMNS |
| Size of a row | 64 KiB | The row is invalid with ROW_TOO_LONG |
| Size of a field | 8 KiB | The row is invalid with FIELD_TOO_LONG |
| Life of a batch | 20 days from POST /batches | A batch still unfinished is cancelled |
| Rows per document | 1,000 items per invoice, 500 services per certificate | The document is invalid with DOCUMENT_TOO_MANY_ROWS |
metadata_<key> columns | 20, each key up to 40 characters of a-z, 0-9 and _, each value up to 500 characters | The row is invalid with METADATA_INVALID |
Defaults
Instead of repeating a value on every row, send it once in defaults, as strings. It takes any document or line column of the type. For each field the row wins, then defaults, then what the JSON endpoint assumes for your company. An unknown column or an invalid value answers 422 VALIDATION_FAILED and creates nothing; GET /batches/{id} returns them normalized.
{
"type": "invoice",
"defaults": {
"receiver_rfc": "CDE170303B61",
"payment_form": "03",
"product_code": "78101800",
"quantity": "1",
"unit_code": "E48",
"tax_object": "02",
"vat_rate": "16%"
}
}external_id,third_party_rfc,third_party_worker_id,notify_email,description,unit_price,metadata_contract_id
INV-2026-000501,HEGL900101K44,,courier-1@example.com,Servicio de entrega a domicilio,1077.586207,CT-0001
INV-2026-000502,,wrk_01m4ra8w0he5esnnsew0v9ffkv,courier-2@example.com,Servicio de entrega a domicilio,862.068966,CT-0002Parties
Each party is identified by its RFC or by its heru id: *_rfc, or one of *_worker_id, *_company_id, *_merchant_id and *_customer_id, as far as the role has that column. heru looks an RFC up among the workers, merchants and customers of your company. The receiver always needs one of them. The issuer too, unless the row has no issuer columns at all, which means the company of your token. A third party, which is always a worker, needs one when the invoice has a third party.
*_worker_external_id (the external_id you gave the worker) is optional and is not enough by itself: it only confirms the party identified by the RFC or the heru id.
| The row carries | Result |
|---|---|
receiver_rfc | The party with that RFC, or the data of the row if it is not registered |
receiver_worker_id | That worker, with the RFC registered in heru |
receiver_rfc and receiver_worker_id | They must be the same party, or PARTY_MISMATCH |
receiver_worker_id and receiver_worker_external_id | They must be the same party, or PARTY_MISMATCH |
| An id that is not of your company | PARTY_MISMATCH |
Only receiver_worker_external_id | The row is invalid with PARTY_IDENTIFIER_REQUIRED on receiver_rfc |
| Neither the RFC nor a heru id | The row is invalid with PARTY_IDENTIFIER_REQUIRED on receiver_rfc |
The same goes for the other roles, with the role's own RFC column. A file with no column able to identify the receiver is not rejected as a whole: each row is invalid with PARTY_IDENTIFIER_REQUIRED, unless defaults gives a value for one.
When an RFC exists as several kinds of party in your company, the kinds the role allows decide; if that leaves more than one, use <role>_kind (company, worker, merchant or customer) or an id, or the row is invalid with PARTY_AMBIGUOUS.
| Case | Result |
|---|---|
| Receiver registered in heru, row with no other data | Stamped with the registered data |
| Receiver registered in heru, row with different data | Stamped with the registered data, with a note in the report. It is not an error. |
| Receiver not registered in heru | Stamped with receiver_name, receiver_tax_regime and receiver_postal_code of the row; without them, RECEIVER_DATA_REQUIRED |
Generic RFC XAXX010101000, written in the row or behind an id | The row is invalid with GENERIC_RFC_NOT_ALLOWED, on receiver_rfc or on the id column |
Generic foreign RFC written in the row: XEXX010101000 | The row is invalid with FOREIGN_RECEIVER_NOT_AVAILABLE |
| Issuer not registered in heru | The row is invalid with ISSUER_NOT_FOUND: without a CSD nothing can be stamped |
| Party inactive, or issuer without an active CSD | The row is invalid with PARTY_INACTIVE. The CSD is checked again when the document is prepared. |
Foreign receiver, on a retention certificate: receiver_country other than MX | The row is invalid with FOREIGN_RECEIVER_NOT_AVAILABLE: not available in v1. Invoices have no receiver_country column |
What heru checks
Batches check format and catalogs. They do not verify the receiver against the SAT. Batch files are expected to contain data already checked with fiscal validations, so check your receivers before you build the file. That applies to invoices and retention certificates alike.
If the stamping authority refuses a document, for instance because of its receiver, that document is failed in the report, with its reason in error_code and error_message, and the rest of the batch goes on. Fix it and send it in a new batch. heru never replaces a receiver with a generic RFC, never applies a different withholding rate because of it and never alters your amounts.
Values
heru corrects a few things and rejects everything else, with the row, the column and a code.
| Value | Accepted | Code when it is not |
|---|---|---|
| SAT keys and postal codes | Filled with zeros on the left to the width of the catalog (3 becomes 03), uppercased, then checked against the catalog | INVALID_CODE |
| Amounts | Decimal point, no thousands separator, no $. Up to 6 decimals in quantity, unit_price, discount, total, taxable, exempt and the base and amount of isr_* and vat_*; up to 2 in platform_tips and the service_* amounts, as in the JSON endpoints | INVALID_DECIMAL, TOO_MANY_DECIMALS |
| Negative amounts | Never. For a discount use discount, which cannot exceed quantity × unit_price | NEGATIVE_AMOUNT, DISCOUNT_EXCEEDS_AMOUNT |
| Rates | A fraction (0.16) or a percentage with its sign (16%, 2.10%). Without %, a value over 1 is rejected as ambiguous | INVALID_RATE, RATE_AMBIGUOUS |
platform_isr_payment_type | The SAT key, or Pago Definitivo for 03 and Pago Provisional for 04 | INVALID_CODE |
| Dates | YYYY-MM-DD | INVALID_DATE |
| Date and time | ISO 8601 with its zone: 2026-10-28T12:00:00-06:00 or …Z | INVALID_DATETIME |
issue_date | Within the 72 hours before stamping, as the SAT allows. Empty means when heru prepares the document, which is what you want for a large file; see processing pace | ISSUE_DATE_OUT_OF_WINDOW |
| Text | Trimmed. RFC and CURP uppercased, with no spaces. Control characters are rejected | INVALID_CHARACTER, INVALID_RFC, INVALID_CURP |
external_id | 1 to 128 characters | REQUIRED, INVALID_FORMAT |
Invoices
One row per item.
| Column | Field of POST /invoices |
|---|---|
external_id * | external_id |
receiver_rfc, receiver_name, receiver_tax_regime, receiver_postal_code | receiver by its SAT data |
receiver_kind, receiver_company_id, receiver_worker_id, receiver_worker_external_id, receiver_merchant_id, receiver_customer_id | receiver by its id. receiver_rfc or a heru id is required; receiver_worker_external_id does not count as one |
issuer_rfc, issuer_kind, issuer_company_id, issuer_merchant_id, issuer_worker_id, issuer_worker_external_id | issuer, by its RFC or heru id |
third_party_rfc, third_party_worker_id, third_party_worker_external_id | third_party, by its RFC or worker id |
notify_email | receiver.email: who gets the PDF and XML by email |
payment_form *, payment_method, cfdi_use, place_of_issue | Same names |
series, folio, issue_date | Only in batches: the series, folio and issue date of the CFDI |
metadata_<key> | metadata. It is not stamped, and it comes back in the report and in webhooks |
description *, quantity *, unit_code *, unit_price *, product_code *, tax_object *, vat_rate | items[] |
discount | Only in batches: the discount of the item |
isr_withholding_rate, vat_withholding_rate | items[].withholdings |
Retention certificates
One row per service. With retention code 26, heru computes the amounts and the withholdings from the services, as POST /retention-certificates does; see the retention certificates guide. If you send them too, heru compares them to within one cent and tells you the value it computed (AMOUNT_MISMATCH).
| Column | Field of POST /retention-certificates |
|---|---|
external_id * | external_id |
receiver_rfc, receiver_name, receiver_tax_regime, receiver_postal_code | receiver by its SAT data |
receiver_curp, receiver_country (empty is MX) | Only in batches: the CURP of the receiver, and its country |
receiver_kind, receiver_company_id, receiver_worker_id, receiver_worker_external_id, receiver_merchant_id, receiver_customer_id | receiver by its id. receiver_rfc or a heru id is required; receiver_worker_external_id does not count as one |
issuer_rfc, issuer_kind, issuer_company_id, issuer_merchant_id | issuer, by its RFC or heru id |
notify_email | receiver.email: who gets the PDF and XML by email |
start_month *, end_month *, year * | period |
retention_code *, description, place_of_issue | Same names |
issue_date | Only in batches: the issue date of the certificate |
metadata_<key> | metadata. It is not stamped, and it comes back in the report and in webhooks |
platform_service_type, platform_periodicity, platform_isr_payment_type, platform_tips, platform_period_from, platform_period_to | platform, with code 26 |
service_date, service_payment_form, service_price_without_vat, service_vat_rate, service_commission_rate, service_commission_amount, service_commission_base, service_government_contribution, service_government_contribution_state | One entry of platform.services, with code 26 |
total, taxable, exempt | amounts: required for other codes; with 26, only to have heru check them |
isr_base, isr_rate, isr_amount, isr_payment_type, vat_base, vat_rate, vat_amount, vat_payment_type | The ISR and VAT entries of withheld_taxes: as above |
external_id,receiver_rfc,receiver_name,receiver_tax_regime,receiver_postal_code,start_month,end_month,year,retention_code,platform_service_type,platform_periodicity,service_date,service_payment_form,service_price_without_vat,service_vat_rate,service_commission_rate,service_commission_amount
RET-2026-09-0001,TAD190521KX1,TACOS EL ALIADO DEMO,601,03100,09,09,2026,26,05,02,2026-09-07,03,18450.00,0.16,0.25,4612.50
RET-2026-09-0001,TAD190521KX1,TACOS EL ALIADO DEMO,601,03100,09,09,2026,26,05,02,2026-09-14,03,21300.00,0.16,0.25,5325.00
RET-2026-09-0002,HEGL900101K44,JOSE LUIS HERNANDEZ GARCIA,625,06600,09,09,2026,26,02,02,2026-09-06,03,2850.00,0.16,0.10,285.00
RET-2026-09-0002,HEGL900101K44,JOSE LUIS HERNANDEZ GARCIA,625,06600,09,09,2026,26,02,02,2026-09-27,09,500.00,,,Large files
upload_url is a resumable upload session, valid until upload_expires_at. One PUT with the whole file works. For a large file, compress it and send it in parts, so a dropped connection costs you one part and not the whole upload.
Gzip. Compress the CSV with gzip and upload it as it is: heru detects it from its content, so you do not need Content-Encoding. CSV compresses well, usually to a fifth or less.
gzip -k invoices.csv
curl -X PUT -T invoices.csv.gz "$UPLOAD_URL"Parts. Send each part with Content-Range. Every part except the last must be a multiple of 256 KiB. heru answers 308 until the last byte arrives, and the batch stays awaiting_upload until then. This file has 300 MiB:
curl -i -X PUT -T part-1 -H "Content-Range: bytes 0-268435455/*" "$UPLOAD_URL"
curl -i -X PUT -T part-2 -H "Content-Range: bytes 268435456-314572799/314572800" "$UPLOAD_URL"Resume. Ask how much arrived with an empty PUT. The Range header of the 308 answer says the last byte received, and you continue from the next one.
curl -i -X PUT -H "Content-Length: 0" -H "Content-Range: bytes */*" "$UPLOAD_URL"If the file has not fully arrived by upload_expires_at, the batch goes invalid with UPLOAD_EXPIRED: create a new one.
Progress
GET /batches/{id} counts rows and documents separately, because one document can take several rows. succeeded, failed, cancelled and pending_review count documents.
| Status | Follow |
|---|---|
validating | validated_rows against total_rows. Totals are final once the whole file was read, and everything is final at ready or invalid. |
processing, paused | processed_documents against valid_documents, and in_flight_documents |
done, cancelled | total_documents = invalid_documents + duplicates + succeeded + failed + cancelled + pending_review |
Processing pace
heru processes batches at its own pace, with a quota per company and a cap shared by all its clients, so that no batch takes over the stamping capacity of everyone else. Splitting a file into several batches does not make it faster.
- A
readybatch carries an estimate. Once it isready,estimated_completion_atsays when heru expects it to finish andfits_lifetimewhether that date is within the 20 days counted fromcreated_at. Both come from the pace available to your company at that moment. It is an estimate, not a promise: the pace can change when other work runs, and a batch still unfinished after 20 days is cancelled, even if it saidfits_lifetime: true. Beforereadyboth arenull. - Plan for days or weeks, within 20 days. Validating a file at the limit can take around an hour, and a file of tens of millions of documents takes weeks to stamp. A batch lives 20 days at most, so if you have a date to meet or a file that may not fit, talk to your account team before uploading it.
- Events arrive as documents are stamped. You do not need to wait for
batch.done: each document sends its own webhook. - Progress can stop for a while with the batch still
processing. When the PAC or the SAT do not answer, or when heru has to slow down, nothing is lost: documents wait and are retried, and a retry does not fail the document. - A batch can outlive
issue_date. The SAT allows stamping up to 72 hours after the issue date, and heru checks it again just before stamping. A document that left the window endsfailedwithISSUE_DATE_OUT_OF_WINDOW. For a large file, leaveissue_dateempty: the document takes the moment heru prepares it, and it does not expire.
A batch that will not finish
If fits_lifetime is false, POST /batches/{id}/process answers 409 BATCH_EXCEEDS_CAPACITY by default and stamps nothing, so a batch known not to finish does not start. You have two ways out:
- Split the file into several batches that fit, or ask your account team for more capacity.
- Start it anyway with
allow_partial. heru stamps what fits and, 20 days aftercreated_at, cancels what is still pending, with the same semantics as cancel.
{ "allow_partial": true }While emission is paused for maintenance, a ready batch may report fits_lifetime false and process answer 409 BATCH_EXCEEDS_CAPACITY until it resumes. allow_partial still starts it.
The body is optional. Without it, or with false, the default applies.
Pause, resume and cancel
POST /batches/{id}/pause, /resume and /cancel take no body and no Idempotency-Key: repeating one is safe. They answer 202 with the batch, or 409 BATCH_INVALID_STATE when its state does not allow them, and change nothing else. process is in the table to show where it stands: where the table says 409 it answers 409 BATCH_NOT_READY. A batch in awaiting_upload or validating can still reach ready; one that is invalid, cancelling, cancelled or done never will, so upload the file to a new batch.
| Status | process | pause | resume | cancel |
|---|---|---|---|---|
awaiting_upload | 409 | 409 | 409 | Goes cancelled, no report |
validating | 409 | 409 | 409 | Goes cancelling, no report |
ready | Goes processing | 409 | 409 | Goes cancelling, with a report |
processing | 202, no change | Goes paused | 202, no change | Goes cancelling |
paused | 202, no change | 202, no change | Goes processing | Goes cancelling |
cancelling, cancelled | 409 | 409 | 409 | 202, no change |
done, invalid | 409 | 409 | 409 | 409 |
In the last moment of processing, once every document has finished and the report is being written, pause, resume and cancel answer 409: the batch is about to be done.
Pause stops heru from preparing more documents. The batch shows paused at once, but documents already prepared still go to the PAC, so the counters can advance for a few minutes; in_flight_documents says how many are left. They finish so that they do not sit aging toward the 72-hour limit during a long pause. Only a batch in processing can be paused, because validating stamps nothing and a ready batch waits for you anyway, up to its 20 days. A pause does not stop those 20 days. If nothing is left to do when you pause, the batch still reaches done.
Resume continues from where it stopped.
Cancel stops what is pending. It never cancels a CFDI.
- Documents already stamped stay stamped and appear in the report. To cancel one, use its own
DELETEendpoint. - Documents on their way to the PAC finish, and end
stampedorfailed. - Everything else becomes
cancelledwitherror_codeBATCH_CANCELLED. A document that was already prepared loses the folio reserved for it, which leaves a gap in that series. cancellingcan last tens of minutes on a very large file, while heru writes the report.- If a document still has no outcome after a bounded wait, it is
pending_reviewand the batch closes. If it gets stamped later, its webhook arrives. batch.donefires once, withstatuscancelled, the counters andreport_url, if the batch had reachedprocess. There is no separate event. A batch cancelled inreadyhas a report but no event, and one cancelled beforereadyhas neither, and its counters stay partial.
To finish what was left, upload the same file to a new batch: what was stamped comes back duplicate and only the rest is stamped. If everything was already stamped, every document is a duplicate: the batch ends done with its report and stamps nothing, and batch.done fires with status done and its report_url even though you never called process. A batch with no valid document that mixes duplicates with documents that failed validation is invalid.
A batch lives 20 days at most. They count from created_at, which is the moment of POST /batches, whatever its status, paused or not. A batch still unfinished at that point is cancelled exactly as if you had called cancel in its current status: same statuses, same BATCH_CANCELLED for what was pending, same report and same batch.done rule.
Errors
errors holds the first 100 problems, in row order, each with row, column, code and reason. The header is row 1, so the first data row is row 2. error_count has the total, and errors_url a gzip CSV with every problem (row,column,code,message), signed for 15 minutes: a new GET gives you a new link. The same goes for the link in the batch.done event: its 15 minutes count from when the event was written, so a late event or a late retry needs a GET /batches/{id}. A problem with the file itself has row 0 and an empty column, and the batch goes invalid. Messages name the column and never repeat an RFC or a CURP.
{
"status": "ready",
"created_at": "2026-10-28T15:04:05Z",
"estimated_completion_at": "2026-11-02T18:00:00Z",
"fits_lifetime": true,
"total_rows": 6,
"validated_rows": 6,
"valid_rows": 5,
"invalid_rows": 1,
"total_documents": 3,
"valid_documents": 2,
"invalid_documents": 1,
"duplicates": 0,
"errors": [
{ "row": 7, "column": "service_commission_amount", "code": "RULE_VIOLATION", "reason": "must equal service_commission_base × service_commission_rate (2475.00), within one cent" }
],
"error_count": 1,
"errors_url": "https://files.heru.app/batches/bat_01m4ra8wpbev7sf851m6pn6gm7-errors.csv.gz"
}| Level | Codes |
|---|---|
File: the batch goes invalid | ENCODING_INVALID, FILE_EMPTY, HEADER_MISSING, HEADER_DUPLICATE, REQUIRED_COLUMN_MISSING, TOO_MANY_COLUMNS, TOO_MANY_ROWS, FILE_TOO_LARGE, CSV_MALFORMED, UPLOAD_EXPIRED, PROCESSING_ERROR |
| Row | CSV_MALFORMED, COLUMN_COUNT_MISMATCH, ROW_TOO_LONG, FIELD_TOO_LONG, INVALID_CHARACTER |
| Value | REQUIRED, INVALID_FORMAT, INVALID_DECIMAL, TOO_MANY_DECIMALS, NEGATIVE_AMOUNT, DISCOUNT_EXCEEDS_AMOUNT, INVALID_RATE, RATE_AMBIGUOUS, INVALID_DATE, INVALID_DATETIME, ISSUE_DATE_OUT_OF_WINDOW, INVALID_CODE, INVALID_RFC, INVALID_CURP, INVALID_POSTAL_CODE, INVALID_EMAIL, INVALID_ID, METADATA_INVALID |
| Document | DOCUMENT_MISMATCH, DOCUMENT_TOO_MANY_ROWS, EXTERNAL_ID_NOT_CONTIGUOUS, FOREIGN_RECEIVER_NOT_AVAILABLE, THIRD_PARTY_NOT_AVAILABLE |
| Parties | PARTY_MISMATCH, PARTY_AMBIGUOUS, PARTY_NOT_ALLOWED, PARTY_INACTIVE, ISSUER_NOT_FOUND, ISSUER_DATA_INVALID, RECEIVER_DATA_REQUIRED, PARTY_IDENTIFIER_REQUIRED, GENERIC_RFC_NOT_ALLOWED |
| SAT rules | RULE_VIOLATION, AMOUNT_MISMATCH |
The report
report_url is a CSV with one line per row of your file, in the same order, signed for 15 minutes: a new GET gives you a new link. The same goes for the link in the batch.done event: its 15 minutes count from when the event was written, so a late event or a late retry needs a GET /batches/{id}. It is stored with gzip and served with Content-Encoding: gzip, so a plain download gives you plain CSV. It has these columns, plus your metadata_*:
row, external_id, status, id, uuid, error_code, error_message, notes
status | The row |
|---|---|
stamped | Belongs to a stamped document, with its id and uuid. Download the files with the /download endpoint of the document. |
invalid | Did not pass validation, and its error_code says why. It was never processed. |
failed | Its document failed when processed, for instance because the stamping authority refused it, with the error_code and error_message of the failure. A valid row of a document with an invalid row is also failed, with error_code INVOICE_ROW_INVALID and a message that names the row to fix. The code name says invoice, but it applies the same way to the rows of a retention_certificate document; that document counts in invalid_documents. |
duplicate | Its external_id was already stamped, with the id and uuid of that document. It is not an error, and nothing was stamped twice. |
cancelled | Never processed because the batch was cancelled, by you or at its 20 days, with error_code BATCH_CANCELLED. |
pending_review | Its document had no outcome when the batch closed. If it gets stamped later, its webhook arrives. |
The stamped data can differ from your row. When the receiver is registered in heru and your row carries a different receiver_name, receiver_tax_regime, receiver_postal_code or receiver_curp, the document is stamped with the registered data and notes says so, naming the columns and never their values:
receiver_data_overridden:receiver_name;receiver_postal_codeIt is a note, not an error: status is stamped, error_code is empty, and receiver_overrides counts these documents. An empty column does not count as different, and the RFC is never overwritten: an id that points to another RFC is PARTY_MISMATCH. The counter is an estimate until the batch closes, because the registered data can change between validating and stamping; the note in the report is what was stamped.
Sending a document again. external_id is unique per company and type across batches, so a repeated external_id comes back duplicate and is never stamped twice. A document already stamped with the same external_id wins over its own validation errors: it is reported duplicate, not invalid, so uploading a file again days later does not fail on rows that were already stamped. EXTERNAL_ID_NOT_CONTIGUOUS still applies. A failed or cancelled document can go again in a new batch.