Issue an invoice
LiveServed by the v1 API in development with the same checks as production; production opens on the same contract. The capabilities from before v1 keep running in production at the path your account team gives you.
Issues a CFDI 4.0.
Who issues it
- Your company: send nothing, or
issuer.company_idto pick one of your companies. - A merchant:
issuer.merchant_id. - A worker with their own CSD:
issuer.worker_id. - heru, on a worker's behalf (the worker needs no CSD):
third_party.worker_id. - Your company, on another taxpayer's behalf:
third_partywith their RFC.
Who receives it
- One of your companies, a worker or a merchant:
receiver.company_id,receiver.worker_idorreceiver.merchant_id. heru fills their SAT data. - Anyone else:
receiverwith their RFC, name, regime and postal code.
Which companies can issue. It depends on the credential: see Companies.
Files. Get the XML or the PDF with GET /invoices/{id}/download, which needs the invoices:download scope.
Synchronous. heru stamps the CFDI with the PAC before answering, usually in 2 to 5 seconds, and returns it with its folio fiscal. Wait up to 30 seconds. If the PAC or the SAT do not answer in time you get 503 and nothing is created: retry after Retry-After with the same Idempotency-Key. If the connection drops or you get another 5xx, retry the same way: you get the CFDI that was already stamped, never a second one.
Needs the invoices:write scope.
oauth2 invoices:writeAuthorizationBearer <token>OAuth 2.0 client credentials. Exchange your client_id and client_secret at /oauth/token of the environment for a bearer token valid for one hour. The playground of every operation asks for your own client and keeps it in your browser tab only.
Scope: invoices:write
Idempotency-Key*stringUnique per operation. Retrying with the same key returns the original result for 24 hours.
length <= 255application/json- body
A CFDI 4.0 of type ingreso: for your customers, for the general public, on behalf of a third party, or a global invoice.
external_id*stringYour own id for this invoice, e.g. your order number. Echoed back in every response and webhook.
length <= 128receiver*||||Who receives the invoice: one of your companies, a worker, a merchant, a customer, or any taxpayer by their data. Name, postal code and regime must match the SAT records exactly.
items*array<>1 <= itemspayment_form*stringSAT c_FormaPago, e.g. 01 cash, 03 transfer, 04 credit card, 28 debit card. 99 (to be defined) only with PPD.
type?stringSAT c_TipoDeComprobante:
IIngreso: a sale or a service. Available.EEgreso: a credit note, discount or return on an earlier invoice. Planned.PPago: payment receipt (complemento de pagos) for an invoice issued withPPD. Planned.TTraslado: moving goods without a sale (carta porte). Planned.NNómina: payroll. Planned.
Until a type is available, it answers 422 with INVOICE_TYPE_NOT_AVAILABLE.
"I""I""E""P""T""N"payment_method?stringSAT c_MetodoPago: PUE paid in one payment, PPD paid in installments or later. Payment receipts (type P) for PPD invoices are planned.
"PUE""PUE""PPD"cfdi_use?stringSAT c_UsoCFDI: what the receiver uses it for. G03 general expenses, S01 no tax effects (general public).
"G03"issuer?||Who issues and seals the CFDI with their CSD. Send exactly one id. Leave issuer out to issue as the company of your credential.
third_party?||The taxpayer on whose behalf the CFDI is issued (por cuenta de terceros). The income is theirs, but they do not seal it, so they need no CSD.
place_of_issue?stringPostal code where the invoice is issued (LugarExpedicion). Defaults to the issuer's tax address; for a merchant, send its location's postal code.
^[0-9]{5}$related_invoices?array<|>Invoices this one is related to (CfdiRelacionados). To replace an invoice issued with errors: send it here with relationship 04, then cancel the old one with DELETE /invoices/{id}?reason=01&replacement_uuid=….
periodicity?stringSAT c_Periodicidad. Global invoices only, e.g. 04 monthly.
months?stringGlobal invoices only. 01 to 12 for a month, 13 to 18 for a two-month period.
^(0[1-9]|1[0-8])$year?stringYear covered. Global invoices only.
^\d{4}$metadata?Your own key-value data. Returned as sent.
The invoice, stamped by the SAT.
application/json- response
id*string^inv_[0-7][0-9a-hjkmnp-tv-z]{25}$external_id*stringYour own id for this invoice, as you sent it.
status*stringstamped once the SAT accepted it. After a cancellation: cancellation_requested while the receiver must accept it, then cancelled.
"stamped""cancellation_requested""cancelled"type*stringSAT c_TipoDeComprobante of the CFDI, as sent.
"I""E""P""T""N"uuid*stringFolio fiscal the SAT assigned.
uuidverification_url?stringSAT page that verifies the invoice.
uristamp*The SAT stamp (TimbreFiscalDigital). Print it on your own representation of the CFDI.
issuer*receiver*third_party?|The taxpayer on whose behalf it was issued, or null.
place_of_issue?stringPostal code where it was issued.
items*array<>related_invoices?array<>Invoices this one is related to. Empty when there are none.
payment_method*string"PUE""PPD"payment_form*stringcfdi_use*stringcurrency*"MXN""MXN"subtotal*stringSum of the items before taxes.
^[0-9]+\.[0-9]{2}$vat*stringVAT transferred.
^[0-9]+\.[0-9]{2}$withholdings?|Total withheld. Null when nothing is withheld.
total*stringsubtotal + vat − withholdings.
^[0-9]+\.[0-9]{2}$periodicity?stringGlobal invoices only.
months?stringGlobal invoices only.
year?stringGlobal invoices only.
cancellation?|Null until you request a cancellation.
metadata?As you sent it.
created_at*stringdate-timestamped_at*stringdate-timeupdated_at*stringdate-timecurl -X POST 'https://api.dev.b2b.heru.app/v1/invoices' \ -H 'Idempotency-Key: try-it-0001' \ -H 'Content-Type: application/json' \ -d '{ "external_id": "INV-2026-000245", "receiver": { "rfc": "HEGL900101K44", "name": "JOSE LUIS HERNANDEZ GARCIA", "tax_regime": "612", "postal_code": "06600", "email": "joseluis.hernandez@cliente.example" }, "items": [ { "description": "Servicio de consultoría", "quantity": 1, "unit_code": "E48", "unit_price": "1000.00", "product_code": "84111506", "tax_object": "02" } ], "payment_form": "03"}'{ "id": "inv_01m4ra8w8mfa8svf5396b2p5e1", "external_id": "INV-2026-000245", "status": "stamped", "type": "I", "uuid": "5829F7E9-4996-450C-BE13-9EE83FD56065", "verification_url": "https://verificacfdi.facturaelectronica.sat.gob.mx/default.aspx?id=5829F7E9-4996-450C-BE13-9EE83FD56065&re=PDE180514R21&rr=HEGL900101K44&tt=1160.00&fe=ydyupw==", "stamp": { "sat_certificate_number": "00001000000509465028", "issuer_certificate_number": "00001000000712345678", "pac_rfc": "SPR190613I52", "issuer_seal": "GHbnil3a0FVwE7ynElV7a/eMdiNsWGBg72T2sHjC6CSRg4CIP947uTmefzvTKPcAoTI9NKFKCAVRf/i6w0HP+Op7Twhh/i2cHOGUUMfWDh/ILDldff/tt7OVzRiunV/Mb8FIWL7yjvU4qWeLzhKSi3WhAAKI9wJW9DBBsid3Q9566BqAldbGgN9YIoPNTdFKPLj9CXK5uSWqnt5jcllVUx3CscBVdoxpCHXfGUN9blsdOXKQ8SV3lv4weMup2+moMzsHXiNTXR6iAkk+WycILJfBGOXk3EIu8+a2fp/IOW+KL6wmPcEEqumiNkK8LeXXtljmVnFMi236w0Fuydyupw==", "sat_seal": "i0o/7TJngD+XRxvsWYHvdAEuw4ozKSJToKkCo6M054CavGKcbwBzzoDvmKQXHsgOCtyRdv9aOGG/UrqBnAffWF36C2TatEaD1b4Y5A6X7KIROheWwr/5zbddOflrlufSQIySnPEzCmE+sgQ7So7RqXJmtpEGW+eXwjRH1YoF2h0TQ5jqGkpTSTjiyoRvMWvC/FPC9O9HJtsXiRHLxWWzQGvsWAXdYNwFYVvI4oXzj38quDonetE+bUi+Qu7KA/xJQdEwsvhdUn7JLoqzXqcKnp2306uy7GFD7Wl/1tMIBPiJqX8x06qXWMEa0WFWM3gWzI0PVtXRoBjYDF8pdwoHpA==", "original_chain": "||1.1|5829F7E9-4996-450C-BE13-9EE83FD56065|2026-10-12T09:04:08|SPR190613I52|GHbnil3a0FVwE7ynElV7a/eMdiNsWGBg72T2sHjC6CSRg4CIP947uTmefzvTKPcAoTI9NKFKCAVRf/i6w0HP+Op7Twhh/i2cHOGUUMfWDh/ILDldff/tt7OVzRiunV/Mb8FIWL7yjvU4qWeLzhKSi3WhAAKI9wJW9DBBsid3Q9566BqAldbGgN9YIoPNTdFKPLj9CXK5uSWqnt5jcllVUx3CscBVdoxpCHXfGUN9blsdOXKQ8SV3lv4weMup2+moMzsHXiNTXR6iAkk+WycILJfBGOXk3EIu8+a2fp/IOW+KL6wmPcEEqumiNkK8LeXXtljmVnFMi236w0Fuydyupw==|00001000000509465028||" }, "issuer": { "rfc": "PDE180514R21", "name": "PLATAFORMA DEMO", "tax_regime": "601", "postal_code": "11000" }, "receiver": { "rfc": "HEGL900101K44", "name": "JOSE LUIS HERNANDEZ GARCIA", "tax_regime": "612", "postal_code": "06600", "email": "joseluis.hernandez@cliente.example" }, "third_party": null, "place_of_issue": "11000", "items": [ { "description": "Servicio de consultoría", "quantity": 1, "unit_code": "E48", "unit_price": "1000.00", "product_code": "84111506", "tax_object": "02", "vat_rate": "0.16", "subtotal": "1000.00", "vat": "160.00", "withholdings": null } ], "related_invoices": [], "payment_method": "PUE", "payment_form": "03", "cfdi_use": "G03", "currency": "MXN", "subtotal": "1000.00", "vat": "160.00", "withholdings": null, "total": "1160.00", "cancellation": null, "metadata": {}, "created_at": "2026-10-12T15:04:05.208Z", "stamped_at": "2026-10-12T15:04:08.422Z", "updated_at": "2026-10-12T15:04:08.422Z"}Get a signatureGET
Returns the signature request. Once the worker signs, `signed_document` carries a signed link to the signed PDF, valid for 15 minutes, and `certificate` the e.firma they used. Get the signature again for a new link. Needs the `signatures:read` scope.
Cancel an invoiceDEL
Requests the cancellation with the SAT. Invoices above 1,000 MXN to a taxpayer need the receiver's acceptance: the status stays `cancellation_requested` until they accept, and `invoice.cancelled` tells you when it is done. Smaller invoices and those to the general public are cancelled right away. The invoice is not deleted: it stays with status `cancelled` and its `cancellation` details. Needs the `invoices:cancel` scope.