heruAPI

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

  1. POST /batches with the type of the rows and, if you want, defaults. It answers with upload_url and upload_expires_at, 24 hours later. Only this call returns upload_url, because the URL is a write credential: GET /batches/{id} always answers it as null. If you lost it, repeat POST /batches with the same Idempotency-Key.
  2. PUT the CSV to upload_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 goes validating.
  3. GET /batches/{id} until it is ready, done when every document is a duplicate, or invalid when no document is valid and at least one failed validation. A ready batch shows what can and what cannot be processed, before anything is stamped: valid_documents and invalid_documents (and valid_rows and invalid_rows), the first 100 problems in errors with the row, the column and a code, all of them in errors_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.
  4. POST /batches/{id}/process stamps the valid documents. If fits_lifetime is false it answers 409 BATCH_EXCEEDS_CAPACITY and starts nothing, unless you ask for allow_partial. The batch goes processing, then done. Each stamped document sends its own event (invoice.stamped or retention_certificate.stamped) as soon as it is stamped, and batch.done fires when every valid document has its outcome. You can pause, resume or cancel it on the way.
  5. Read report_url for the result of every row. The report_url (and errors_url) inside the batch.done event 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, call GET /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.

typeEach row isScope besides batches:write
invoiceOne item of an invoiceinvoices:write
retention_certificateOne service of a retention certificateretention_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

LimitValueOver the limit
Data rows per file100,000,000The batch goes invalid with TOO_MANY_ROWS
File size, uncompressed32 GiBThe batch goes invalid with FILE_TOO_LARGE
File size as uploaded32 GiB, in upload_max_bytesThe upload is refused
Columns256The batch goes invalid with TOO_MANY_COLUMNS
Size of a row64 KiBThe row is invalid with ROW_TOO_LONG
Size of a field8 KiBThe row is invalid with FIELD_TOO_LONG
Life of a batch20 days from POST /batchesA batch still unfinished is cancelled
Rows per document1,000 items per invoice, 500 services per certificateThe document is invalid with DOCUMENT_TOO_MANY_ROWS
metadata_<key> columns20, each key up to 40 characters of a-z, 0-9 and _, each value up to 500 charactersThe 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.

POST /batches
{
  "type": "invoice",
  "defaults": {
    "receiver_rfc": "CDE170303B61",
    "payment_form": "03",
    "product_code": "78101800",
    "quantity": "1",
    "unit_code": "E48",
    "tax_object": "02",
    "vat_rate": "16%"
  }
}
invoices.csv
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-0002

Parties

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 carriesResult
receiver_rfcThe party with that RFC, or the data of the row if it is not registered
receiver_worker_idThat worker, with the RFC registered in heru
receiver_rfc and receiver_worker_idThey must be the same party, or PARTY_MISMATCH
receiver_worker_id and receiver_worker_external_idThey must be the same party, or PARTY_MISMATCH
An id that is not of your companyPARTY_MISMATCH
Only receiver_worker_external_idThe row is invalid with PARTY_IDENTIFIER_REQUIRED on receiver_rfc
Neither the RFC nor a heru idThe 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.

CaseResult
Receiver registered in heru, row with no other dataStamped with the registered data
Receiver registered in heru, row with different dataStamped with the registered data, with a note in the report. It is not an error.
Receiver not registered in heruStamped 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 idThe row is invalid with GENERIC_RFC_NOT_ALLOWED, on receiver_rfc or on the id column
Generic foreign RFC written in the row: XEXX010101000The row is invalid with FOREIGN_RECEIVER_NOT_AVAILABLE
Issuer not registered in heruThe row is invalid with ISSUER_NOT_FOUND: without a CSD nothing can be stamped
Party inactive, or issuer without an active CSDThe 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 MXThe 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.

ValueAcceptedCode when it is not
SAT keys and postal codesFilled with zeros on the left to the width of the catalog (3 becomes 03), uppercased, then checked against the catalogINVALID_CODE
AmountsDecimal 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 endpointsINVALID_DECIMAL, TOO_MANY_DECIMALS
Negative amountsNever. For a discount use discount, which cannot exceed quantity × unit_priceNEGATIVE_AMOUNT, DISCOUNT_EXCEEDS_AMOUNT
RatesA fraction (0.16) or a percentage with its sign (16%, 2.10%). Without %, a value over 1 is rejected as ambiguousINVALID_RATE, RATE_AMBIGUOUS
platform_isr_payment_typeThe SAT key, or Pago Definitivo for 03 and Pago Provisional for 04INVALID_CODE
DatesYYYY-MM-DDINVALID_DATE
Date and timeISO 8601 with its zone: 2026-10-28T12:00:00-06:00 or …ZINVALID_DATETIME
issue_dateWithin 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 paceISSUE_DATE_OUT_OF_WINDOW
TextTrimmed. RFC and CURP uppercased, with no spaces. Control characters are rejectedINVALID_CHARACTER, INVALID_RFC, INVALID_CURP
external_id1 to 128 charactersREQUIRED, INVALID_FORMAT

Invoices

One row per item.

ColumnField of POST /invoices
external_id *external_id
receiver_rfc, receiver_name, receiver_tax_regime, receiver_postal_codereceiver by its SAT data
receiver_kind, receiver_company_id, receiver_worker_id, receiver_worker_external_id, receiver_merchant_id, receiver_customer_idreceiver 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_idissuer, by its RFC or heru id
third_party_rfc, third_party_worker_id, third_party_worker_external_idthird_party, by its RFC or worker id
notify_emailreceiver.email: who gets the PDF and XML by email
payment_form *, payment_method, cfdi_use, place_of_issueSame names
series, folio, issue_dateOnly 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_rateitems[]
discountOnly in batches: the discount of the item
isr_withholding_rate, vat_withholding_rateitems[].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).

ColumnField of POST /retention-certificates
external_id *external_id
receiver_rfc, receiver_name, receiver_tax_regime, receiver_postal_codereceiver 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_idreceiver 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_idissuer, by its RFC or heru id
notify_emailreceiver.email: who gets the PDF and XML by email
start_month *, end_month *, year *period
retention_code *, description, place_of_issueSame names
issue_dateOnly 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_toplatform, 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_stateOne entry of platform.services, with code 26
total, taxable, exemptamounts: 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_typeThe ISR and VAT entries of withheld_taxes: as above
retention-certificates.csv
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.

StatusFollow
validatingvalidated_rows against total_rows. Totals are final once the whole file was read, and everything is final at ready or invalid.
processing, pausedprocessed_documents against valid_documents, and in_flight_documents
done, cancelledtotal_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 ready batch carries an estimate. Once it is ready, estimated_completion_at says when heru expects it to finish and fits_lifetime whether that date is within the 20 days counted from created_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 said fits_lifetime: true. Before ready both are null.
  • 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 ends failed with ISSUE_DATE_OUT_OF_WINDOW. For a large file, leave issue_date empty: 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 after created_at, cancels what is still pending, with the same semantics as cancel.
POST /batches/{id}/process
{ "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.

Statusprocesspauseresumecancel
awaiting_upload409409409Goes cancelled, no report
validating409409409Goes cancelling, no report
readyGoes processing409409Goes cancelling, with a report
processing202, no changeGoes paused202, no changeGoes cancelling
paused202, no change202, no changeGoes processingGoes cancelling
cancelling, cancelled409409409202, no change
done, invalid409409409409

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 DELETE endpoint.
  • Documents on their way to the PAC finish, and end stamped or failed.
  • Everything else becomes cancelled with error_code BATCH_CANCELLED. A document that was already prepared loses the folio reserved for it, which leaves a gap in that series.
  • cancelling can 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_review and the batch closes. If it gets stamped later, its webhook arrives.
  • batch.done fires once, with status cancelled, the counters and report_url, if the batch had reached process. There is no separate event. A batch cancelled in ready has a report but no event, and one cancelled before ready has 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.

GET /batches/{id}
{
  "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"
}
LevelCodes
File: the batch goes invalidENCODING_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
RowCSV_MALFORMED, COLUMN_COUNT_MISMATCH, ROW_TOO_LONG, FIELD_TOO_LONG, INVALID_CHARACTER
ValueREQUIRED, 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
DocumentDOCUMENT_MISMATCH, DOCUMENT_TOO_MANY_ROWS, EXTERNAL_ID_NOT_CONTIGUOUS, FOREIGN_RECEIVER_NOT_AVAILABLE, THIRD_PARTY_NOT_AVAILABLE
PartiesPARTY_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 rulesRULE_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

statusThe row
stampedBelongs to a stamped document, with its id and uuid. Download the files with the /download endpoint of the document.
invalidDid not pass validation, and its error_code says why. It was never processed.
failedIts 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.
duplicateIts external_id was already stamped, with the id and uuid of that document. It is not an error, and nothing was stamped twice.
cancelledNever processed because the batch was cancelled, by you or at its 20 days, with error_code BATCH_CANCELLED.
pending_reviewIts 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_code

It 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.

© 2026 Kuarto Inc. (heru)heru.appQuestions about your integration? Contact your heru account team.

On this page