Descargar archivos de transacciones
De punta a punta, cómo elegir las transacciones, obtener sus tickets, facturas y XML, y descargarlos verificando su integridad.
Cada transacción puede tener tickets (fotos o comprobantes no fiscales), facturas en formato imprimible (por ejemplo, PDF) y comprobantes fiscales digitales en XML (por ejemplo, el CFDI en México). Esta receta descarga todos los archivos de un conjunto de transacciones en tres pasos:
- Buscar las transacciones con
GET /purchases. - Pedir sus archivos con
GET /purchases/files, de a 100 transacciones por consulta. - Descargar cada archivo desde su URL de descarga y verificar su hash.
1. Elige las transacciones
Filtra GET /purchases por lo que necesites y guarda el uniqueCode de cada transacción.
dateFrom y dateTo filtran por fecha de pago. Para traer las transacciones creadas o
modificadas desde cierto momento, usa lastModifiedDateFrom. También puedes filtrar por
compañía, comercio, estado y otros campos (ver la referencia de GET /purchases). Recorre todas las páginas, como se explica en
Paginación:
curl -g -X GET 'https://{HOST}/api/public/v1/purchases?dateFrom=2026-10-01T00:00:00Z&size=500&page=0&sort[0].property=createdDate&sort[0].direction=DESC' \
-H 'X-API-KEY: tu_api_key_aqui'Las fechas van en formato ISO 8601 con zona horaria: Z o un offset como
-06:00. En la URL, el + de un offset positivo se escribe %2B. Sin zona, la API
responde 400.
Si tu key tiene acceso a varias compañías, agrega companyCode para limitar la búsqueda
(ver Uso multi-compañía).
2. Pide los archivos de esas transacciones
Pasa los uniqueCode en purchaseCodes, repitiendo el parámetro, en tandas de hasta
100 transacciones:
curl -X GET 'https://{HOST}/api/public/v1/purchases/files?purchaseCodes=COMPRA_1&purchaseCodes=COMPRA_2&size=100' \
-H 'X-API-KEY: tu_api_key_aqui'La respuesta trae un elemento por cada transacción que tiene archivos. Las transacciones sin archivos no aparecen, así que no esperes un elemento por cada código que enviaste:
{
"_embedded": {
"purchasesFiles": [
{
"purchaseCode": "COMPRA_1",
"ticketFiles": [
{
"uniqueCode": "…",
"filename": "ticket.png",
"extension": "png",
"contentType": "image/png",
"sizeInBytes": 16175,
"fileDigest": "9f2c…",
"_links": { "bytes": { "href": "https://…" } }
}
],
"invoiceFiles": [],
"digitalInvoiceFiles": []
}
]
},
"page": { "size": 100, "totalElements": 1, "totalPages": 1, "number": 0 }
}| Campo | Contenido |
|---|---|
ticketFiles | Tickets y comprobantes no fiscales. |
invoiceFiles | Facturas en formato imprimible (por ejemplo, PDF). |
digitalInvoiceFiles | Comprobantes fiscales digitales en XML. |
Las listas vacías pueden venir vacías o no venir: la API omite los campos sin valor.
Trata una lista ausente como vacía. Ignora invoiceFile y digitalInvoiceFile (en
singular): solo repiten el primer elemento de la lista correspondiente.
Para una sola transacción también puedes usar GET /purchases/{code}/files, que devuelve el
mismo objeto sin paginar.
3. Descarga y verifica cada archivo
_links.bytes.href es una URL prefirmada: se descarga con un GET simple, sin
X-API-KEY, y vence pocos minutos después de emitida. Descarga cada archivo apenas
recibes la respuesta del paso 2 y no guardes la URL para después: si venció, vuelve a
pedir los archivos de esa transacción.
curl -o ticket.png 'URL_DE_BYTES_HREF'fileDigest es el SHA-256 del contenido en hexadecimal. Compáralo con el hash del
archivo descargado para confirmar que llegó completo:
sha256sum ticket.pngEl mismo fileDigest en dos archivos indica que son el mismo contenido.
La receta completa
// Node.js 18 o superior. Descarga los archivos de las transacciones pagadas desde una fecha.
import { createHash } from 'node:crypto';
import { mkdir, writeFile } from 'node:fs/promises';
const HOST = 'https://mx.stage.app.mendel.com';
const headers = { 'X-API-KEY': process.env.MENDEL_API_KEY, Accept: 'application/json' };
async function get(path) {
const res = await fetch(`${HOST}/api/public/v1${path}`, { headers });
if (!res.ok) throw new Error(`${res.status} en ${path}`);
return res.json();
}
// 1. Los uniqueCode de las transacciones, recorriendo todas las páginas.
async function purchaseCodes(filters) {
const codes = [];
for (let page = 0, totalPages = 1; page < totalPages; page++) {
const query = new URLSearchParams({
...filters, page, size: 500,
'sort[0].property': 'createdDate', 'sort[0].direction': 'DESC',
});
const body = await get(`/purchases?${query}`);
codes.push(...(body._embedded?.purchases ?? []).map((p) => p.uniqueCode));
totalPages = body.page.totalPages;
}
return codes;
}
// 2. Sus archivos, en tandas de 100 transacciones.
async function* purchaseFiles(codes) {
for (let i = 0; i < codes.length; i += 100) {
const query = new URLSearchParams({ size: 100 });
for (const code of codes.slice(i, i + 100)) query.append('purchaseCodes', code);
const body = await get(`/purchases/files?${query}`);
yield* body._embedded?.purchasesFiles ?? [];
}
}
// 3. Descarga cada archivo (sin la API key) y verifica su hash.
const codes = await purchaseCodes({ dateFrom: '2026-10-01T00:00:00Z' });
for await (const purchase of purchaseFiles(codes)) {
const files = [
...(purchase.ticketFiles ?? []),
...(purchase.invoiceFiles ?? []),
...(purchase.digitalInvoiceFiles ?? []),
];
await mkdir(`archivos/${purchase.purchaseCode}`, { recursive: true });
for (const file of files) {
const res = await fetch(file._links.bytes.href);
if (!res.ok) throw new Error(`${res.status} al descargar ${file.uniqueCode}`);
const bytes = Buffer.from(await res.arrayBuffer());
const digest = createHash('sha256').update(bytes).digest('hex');
if (digest !== file.fileDigest) throw new Error(`Hash distinto en ${file.uniqueCode}`);
await writeFile(`archivos/${purchase.purchaseCode}/${file.uniqueCode}.${file.extension}`, bytes);
}
}Nombres de archivo
La receta guarda cada archivo con su uniqueCode y su extension, que son únicos y
seguros para el sistema de archivos. filename es el nombre original: puede repetirse
entre archivos y contener caracteres no válidos en una ruta.
Problemas frecuentes
| Síntoma | Causa |
|---|---|
| La respuesta del paso 2 trae menos transacciones que las que enviaste | Las transacciones sin archivos no aparecen. |
| El paso 2 devuelve una página vacía | No enviaste purchaseCodes, o ninguna de esas transacciones tiene archivos o es de tus compañías. |
La descarga responde 403 | La URL venció. Vuelve a pedir los archivos de esa transacción. |
El hash no coincide con fileDigest | La descarga se cortó. Vuelve a descargar el archivo. |
400 en el paso 1 | Revisa el formato de las fechas (con zona horaria) y el de sort. |