Retention certificates
The monthly CFDI de retenciones a platform gives to its merchants and couriers, with the complement for digital platforms.
Every month a platform gives each merchant and courier it withheld taxes from a CFDI de retenciones e información de pagos, with the complemento de servicios de plataformas tecnológicas (retention code 26). This guide covers the whole cycle: who issues it, what you send, how heru computes the withholdings, how to issue a month of certificates in one batch, how to reconcile, and how to correct one.
Where each part runs today
Every operation on this page is Live on the v1 API: run it in the development environment with your own credentials, where the SAT test environment stamps it with the same checks production applies. Production opens on the same contract; until then, heru issues certificates from the monthly file agreed with your account team, who will also agree the switch date with you.
Who issues the certificate
The CFDI de retenciones is issued by whoever withheld: your company. heru seals it with your company's CSD, which it keeps for you (see Companies), and stamps it with its PAC. heru never issues it in its own name, and it cannot issue it as a third party.
The receiver is the merchant or courier, identified in one of two ways:
- by id, if you registered them with heru:
receiver.merchant_idorreceiver.worker_id, and heru fills their RFC, name, regime and postal code; - by their SAT data:
receiver.rfc,name,tax_regimeandpostal_code, exactly as the SAT has them. Validate them first with a fiscal validation: a name or postal code that differs from the SAT is the most common reason a certificate is rejected.
What you send
One certificate per receiver, per month and per service type:
period: one month (start_monthequalsend_month), from 2019 and never later than the current month.retention_code:26.platform.service_type: what the receiver did through your platform. It sets the ISR rate.platform.periodicity: how often you pay them, e.g.weekly.platform.services: the services of the month. Send one entry per service, or add up the services of one day and payment form into one entry; the SAT counts each entry as one service. Each entry has thedate, thepayment_formthe customer used, theprice_without_vat, thevat_rateand your platform'scommission.
You do not send amounts or withheld_taxes: heru computes them the way the SAT checks them. If you want heru to check your own numbers, send them too; any difference answers 422 with the value the SAT expects.
The rates heru applies
service_type (SAT c_TipoDeServ) | What it covers | ISR withheld |
|---|---|---|
01 | Passenger transport (rides) | 2.1 % |
02 | Delivering prepared food (couriers) | 2.1 % |
03 | Delivering other goods | 2.1 % |
04 | Lodging | 4 % |
05 | Selling goods, e.g. a restaurant's food (merchants) | 2.5 % (1 % before 2026) |
06 | Any other service | 2.5 % (1 % before 2026) |
07, 08, 09 | Digital content, online clubs and dating, distance learning | 2.5 % (1 % before 2026) |
- Receivers with a 12-character RFC (personas morales, such as most restaurants): 2.5 % of ISR from 2026, whatever the service type.
- Receivers without an RFC (
XAXX010101000,PUBLICO EN GENERAL): 20 % of ISR and all the VAT. - VAT: half of the VAT transferred on the services. The base of the VAT withholding is the VAT itself, not the price: on a service of $2,850.00 at 16 %, the VAT is $456.00 and you withhold $228.00.
- Cash: services paid in cash (
payment_form01) count in the total but not in the ISR and VAT bases, because the platform did not collect that money. - Bonuses and prizes (
payment_form09) carry no VAT and no commission. - ISR payment type:
04(provisional) unless the courier opted to treat the withholding as their definitive payment; then setplatform.isr_payment_typeto03(definitive).
A merchant and a courier
A restaurant that sold $61,500.00 through your platform in September, one week in cash. heru withholds 2.5 % of ISR on $59,350.00 and half of the $9,496.00 of VAT on the non-cash sales: $6,231.75 in total.
{
"external_id": "RET-2026-000121",
"receiver": { "rfc": "TAD190521KX1", "name": "TACOS EL ALIADO DEMO", "tax_regime": "601", "postal_code": "03100" },
"period": { "start_month": "09", "end_month": "09", "year": "2026" },
"retention_code": "26",
"platform": {
"service_type": "05",
"periodicity": "02",
"services": [
{ "date": "2026-09-07", "payment_form": "03", "price_without_vat": "18450.00", "vat_rate": "0.16", "commission": { "rate": "0.25", "amount": "4612.50" } },
{ "date": "2026-09-14", "payment_form": "03", "price_without_vat": "21300.00", "vat_rate": "0.16", "commission": { "rate": "0.25", "amount": "5325.00" } },
{ "date": "2026-09-21", "payment_form": "01", "price_without_vat": "2150.00", "vat_rate": "0.16", "commission": { "rate": "0.25", "amount": "537.50" } },
{ "date": "2026-09-28", "payment_form": "03", "price_without_vat": "19600.00", "vat_rate": "0.16", "commission": { "rate": "0.25", "amount": "4900.00" } }
]
}
}TAD190521KX1 A courier who earned $8,950.00 in delivery fees and a $500.00 bonus. heru withholds 2.1 % of ISR on $9,450.00 and half of the $1,432.00 of VAT: $914.45.
HEGL900101K44 The answer is 201 with the stamped certificate: its folio fiscal (uuid), the withholdings, the complement with its totals and the SAT stamp. retention_certificate.stamped follows by webhook.
A month of certificates: use a batch
For thousands of receivers, send one CSV a month instead of one request per certificate. A batch of type retention_certificate takes one row per service; rows with the same external_id are one certificate.
POST /batcheswith{"type": "retention_certificate"}andPUTthe CSV toupload_url.- heru checks every certificate against the same rules as
POST /retention-certificatesbefore anything is stamped, except that it does not verify the receiver against the SAT: check your receivers with fiscal validations first. A certificate the stamping authority refuses endsfailedin the report, with its reason.GET /batches/{id}lists inerrorsthe row and column to fix. - Fix the file and upload it to a new batch, or
POST /batches/{id}/processto stamp the valid certificates now and the rest later. - When
batch.donefires,report_urlhas the folio fiscal of every stamped certificate.
6 rows Reconcile the month
GET /retention-certificates?period=2026-09&status=stamped lists every certificate of the month, newest first, 100 per page (up to 500 with limit). Follow next_cursor until it is null. Filter by receiver_rfc, merchant_id, worker_id, company_id or external_id to find one.
Your external_id names one certificate per issuer. Sending it again, with a new Idempotency-Key, answers 409 EXTERNAL_ID_ALREADY_USED with the id of the certificate you already have, so a retry of your monthly job never stamps a second CFDI.
Files
GET /retention-certificates/{id}/download?format=xml (or pdf) returns a signed link valid for 15 minutes. The XML is the document with fiscal validity; the PDF shows the SAT seal and a QR to verify it. The files are never at a public address: request a new link each time a merchant or courier needs theirs.
Correct or cancel a certificate
Retention certificates need no acceptance from the receiver.
- Wrong data, with a replacement. Issue the corrected certificate with the old one in
related_certificates(relationship04, same receiver), thenDELETE /retention-certificates/{old id}?reason=01&replacement_uuid={new uuid}. - Wrong data, no replacement.
DELETE …?reason=02. - The operation did not happen.
DELETE …?reason=03.
DELETE answers 202 with status cancellation_requested. The SAT confirms within seconds: the status becomes cancelled and retention_certificate.cancelled fires.
TAD190521KX1 When something fails
| Answer | What happened | What to do |
|---|---|---|
422 VALIDATION_FAILED | A rule of the CFDI or the complement failed. fields names the field, the value the SAT expects and the SAT rule (e.g. SPT116). | Fix the field and send it again with the same external_id. |
422 SAT_REJECTED | The SAT does not recognize the receiver: RFC, name or postal code. | Validate the receiver with a fiscal validation and use its values. |
422 COMPANY_CSD_NOT_ACTIVE | The CSD heru keeps for your company expired or was revoked. | Send the new CSD to your account team. |
409 EXTERNAL_ID_ALREADY_USED | You already have a certificate with this external_id. | Read it with the id in the message. |
503 STAMP_UNAVAILABLE | The PAC or the SAT did not answer in time. Nothing was stamped. | Retry after Retry-After with the same Idempotency-Key. Batches retry for you. |
Several of these failures are runnable cases on the development page.