Migrating from a PAC? Use stampxml
If your system already generates and seals the CFDI 4.0 XML, you can migrate to Factura Digital without rewriting that part. You send the same sealed XML to invoice/stampxml or invoice/stampxmlfile and the API stamps it. This guide describes the changes to your integration and how to test them in the sandbox before using your production certificate.
/invoice/stampxml/invoice/stampxmlfileWhat stays and what changes
| In your integration | |
|---|---|
| XML and original string generation | They stay. Your system keeps generating the CFDI 4.0 according to the SAT's Anexo 20. |
| Seal with your CSD | It stays. You do not need to upload your CSD to the account to stamp. |
| URL and authentication | They change. Requests go to the API v5 with the X-Api-Key header; details are in Keys and environments. |
| Sending the XML | It changes. The XML travels in Base64 in the xmlbase64 field (stampxml) or as a file in the xml field of a multipart request (stampxmlfile). |
| Response | It changes. It is a JSON with the UUID, the stamp data and the full stamped XML in cfdi.XmlBase64. |
| PDF and email delivery | They stay in your system. stampxml does not generate a PDF, and the CFDI does not appear in your account listings or get sent with invoice/send. |
| Cancellation | It is done with invoice/cancelwithcsd, sending your CSD in the request. See the cancellation guide. |
Migration steps
1. Create your sandbox account
The sandbox is free and uses the same API as production. Get the API key of your sandbox account and set it up with the sandbox base URL, as described in Keys and environments.
2. Test the connection with a test XML
Generate a sealed XML in the test XML generator and send it to the sandbox invoice/stampxml. An HTTP 200 response confirms that the key, the URL and the request format are correct. The generator includes the send commands with the downloaded file name.
3. Check your system's sealing
Generate an XML with the “Unsealed” option, download the test issuer's .cer and .key and seal the XML with your code. Then check it in the XML validator, which recomputes the original string with the SAT XSLT and reports whether the seal matches. This confirms that your system seals correctly before you install your production certificate.
4. Replace the stamping call
Replace the call to your current provider with the request to invoice/stampxml. FD_API is the environment base URL and FD_API_KEY is the account API key. To send the file without encoding it, use invoice/stampxmlfile with the xml field.
curl -sS -X POST "$FD_API/invoice/stampxml" \
-H "X-Api-Key: $FD_API_KEY" \
--data-urlencode "xmlbase64=$(base64 < factura-sellada.xml | tr -d '\n')"<?php
$xml = file_get_contents('factura-sellada.xml');
$ch = curl_init(getenv('FD_API') . '/invoice/stampxml');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['X-Api-Key: ' . getenv('FD_API_KEY')],
CURLOPT_POSTFIELDS => http_build_query(['xmlbase64' => base64_encode($xml)]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
]);
$r = json_decode(curl_exec($ch), true);
echo $r['code'], ' ', $r['cfdi']['UUID'] ?? $r['message'], PHP_EOL;import { readFile } from "node:fs/promises";
const xml = await readFile("factura-sellada.xml");
const res = await fetch(`${process.env.FD_API}/invoice/stampxml`, {
method: "POST",
headers: { "X-Api-Key": process.env.FD_API_KEY },
body: new URLSearchParams({ xmlbase64: xml.toString("base64") }),
signal: AbortSignal.timeout(120_000),
});
const r = await res.json();
console.log(r.code, r.cfdi?.UUID ?? r.message);import base64, os, requests
with open("factura-sellada.xml", "rb") as f:
xml = f.read()
r = requests.post(
f"{os.environ['FD_API']}/invoice/stampxml",
headers={"X-Api-Key": os.environ["FD_API_KEY"]},
data={"xmlbase64": base64.b64encode(xml).decode()},
timeout=120,
)
print(r.json())var xml = await File.ReadAllBytesAsync("factura-sellada.xml");
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(120) };
http.DefaultRequestHeaders.Add("X-Api-Key", Environment.GetEnvironmentVariable("FD_API_KEY"));
var body = new FormUrlEncodedContent(new Dictionary<string, string> {
["xmlbase64"] = Convert.ToBase64String(xml),
});
var res = await http.PostAsync($"{Environment.GetEnvironmentVariable("FD_API")}/invoice/stampxml", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());5. Adapt how you read the response
Both methods return the same response. Store the stamped XML from cfdi.XmlBase64, which is the CFDI with the TimbreFiscalDigital.
{
"message": "XML stamped successfully",
"code": 200,
"cfdi": {
"UUID": "6F0C2E1A-9B3D-4C7E-8A51-2D4F6B8C0E13",
"FechaTimbrado": "2026-11-04T10:15:30",
"NoCertificado": "30001000000500003416",
"SelloCFD": "Q2V4...Zm9vYmFy",
"NoCertificadoSAT": "30001000000500003456",
"SelloSAT": "Tm9w...ZGF0YQ==",
"RfcProvCertif": "AAA010101AAA",
"CadenaOrigTFD": "||1.1|6F0C2E1A-9B3D-4C7E-8A51-2D4F6B8C0E13|2026-11-04T10:15:30|…||",
"CadenaQR": "https://verificacfdi.facturaelectronica.sat.gob.mx/default.aspx?id=6F0C2E1A-…",
"XmlBase64": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4..."
}
}cfdi fields
UUID- Tax folio assigned at stamping.
XmlBase64- Full stamped XML in Base64. This is the CFDI you must store.
FechaTimbrado- Stamping date and time.
SelloSAT and NoCertificadoSAT- Stamp seal and SAT certificate number, for your printed representation.
CadenaOrigTFD- Original string of the digital tax stamp, for your PDF.
CadenaQR- SAT verification URL for your PDF's QR code.
6. Handle errors and retries
- HTTP 400 with a SAT code at the start of
messagemeans the XML was rejected. Look up the code in the error matrix and fix the XML. timbre_previomeans that XML was already stamped.context.uuidcarries the original UUID and no other folio is used, so you must not resend it.601means the account has no folios available.5037means the stamping service could not be reached and nothing was sent. You can retry the same request.5034means no response arrived in time and the CFDI may have been stamped. Ifcontext.uuidis in the response, that is the UUID; otherwise, verify before retrying.- Set a timeout of at least 120 seconds in your HTTP client.
7. Move to production
Switch the base URL to production, use your production account API key and seal with your CSD. Each stamped CFDI uses one folio; a rejection does not use folios. The full reference for both methods is in Stamp sealed XML.
Migration tools
- Test XML generator
Sealed CFDI 4.0Generate a CFDI 4.0 with a SAT test RFC and CSD, sealed or unsealed, with the commands to send it to stampxml or stampxmlfile. - XML validator
Seal and original stringCheck the seal against the original string from the SAT XSLT, the certificate, the date and the amounts before stamping. - Error matrix
CFDI40xxxEvery SAT error code and API code, searchable. - Postman collection
API v5Every API endpoint ready to test in the sandbox.
Checked against the production API v5 on 2026-11-03.