{"id":36972015,"url":"https://github.com/phpcfdi/sat-ws-descarga-masiva","last_synced_at":"2026-01-13T21:55:02.792Z","repository":{"id":35056553,"uuid":"199937124","full_name":"phpcfdi/sat-ws-descarga-masiva","owner":"phpcfdi","description":"Librería para usar el servicio web del SAT de Descarga Masiva","archived":false,"fork":false,"pushed_at":"2025-09-27T01:32:22.000Z","size":768,"stargazers_count":144,"open_issues_count":0,"forks_count":65,"subscribers_count":20,"default_branch":"main","last_synced_at":"2025-11-04T18:20:34.831Z","etag":null,"topics":["cfdi","descargamasivasat","sat"],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/phpcfdi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2019-07-31T22:24:52.000Z","updated_at":"2025-10-01T18:34:42.000Z","dependencies_parsed_at":"2023-01-15T12:54:06.534Z","dependency_job_id":"19937627-1664-4d4f-8c23-304e3eb79d5d","html_url":"https://github.com/phpcfdi/sat-ws-descarga-masiva","commit_stats":{"total_commits":453,"total_committers":8,"mean_commits":56.625,"dds":0.08167770419426046,"last_synced_commit":"4f95e0d51d9343e9a31a5097195e15dde2c50d4e"},"previous_names":[],"tags_count":28,"template":false,"template_full_name":null,"purl":"pkg:github/phpcfdi/sat-ws-descarga-masiva","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phpcfdi%2Fsat-ws-descarga-masiva","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phpcfdi%2Fsat-ws-descarga-masiva/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phpcfdi%2Fsat-ws-descarga-masiva/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phpcfdi%2Fsat-ws-descarga-masiva/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/phpcfdi","download_url":"https://codeload.github.com/phpcfdi/sat-ws-descarga-masiva/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phpcfdi%2Fsat-ws-descarga-masiva/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28401954,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-13T14:36:09.778Z","status":"ssl_error","status_checked_at":"2026-01-13T14:35:19.697Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cfdi","descargamasivasat","sat"],"created_at":"2026-01-13T21:55:02.645Z","updated_at":"2026-01-13T21:55:02.771Z","avatar_url":"https://github.com/phpcfdi.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# phpcfdi/sat-ws-descarga-masiva\n\n[![Source Code][badge-source]][source]\n[![Packagist PHP Version Support][badge-php-version]][php-version]\n[![Discord][badge-discord]][discord]\n[![Latest Version][badge-release]][release]\n[![Software License][badge-license]][license]\n[![Build Status][badge-build]][build]\n[![Reliability][badge-reliability]][reliability]\n[![Maintainability][badge-maintainability]][maintainability]\n[![Code Coverage][badge-coverage]][coverage]\n[![Violations][badge-violations]][violations]\n[![Total Downloads][badge-downloads]][downloads]\n\n\u003e Librería para usar el servicio web del SAT de Descarga Masiva\n\n:us: The documentation of this project is in spanish as this is the natural language for intented audience.\n\n:mexico: La documentación del proyecto está en español porque ese es el lenguaje principal de los usuarios.\nTambién te esperamos en [el canal #phpcfdi de discord](https://discord.gg/aFGYXvX)\n\nEsta librería contiene un cliente (consumidor) del servicio del SAT de\n**Servicio Web de Descarga Masiva de CFDI y Retenciones** versión 1.5 (2025-05-30).\n\n## Instalación\n\nUtiliza [composer](https://getcomposer.org/), instala de la siguiente forma:\n\n```shell\ncomposer require phpcfdi/sat-ws-descarga-masiva\n```\n\n## Ejemplos de uso\n\nTodos los objetos de entrada y salida se pueden exportar como JSON para su fácil depuración.\n\n### Creación el servicio\n\nEjemplo creando el servicio usando una FIEL disponible localmente.\n\n```php\n\u003c?php\n\nuse PhpCfdi\\SatWsDescargaMasiva\\RequestBuilder\\FielRequestBuilder\\Fiel;\nuse PhpCfdi\\SatWsDescargaMasiva\\RequestBuilder\\FielRequestBuilder\\FielRequestBuilder;\nuse PhpCfdi\\SatWsDescargaMasiva\\Service;\nuse PhpCfdi\\SatWsDescargaMasiva\\WebClient\\GuzzleWebClient;\n\n// Creación de la FIEL, puede leer archivos DER (como los envía el SAT) o PEM (convertidos con openssl)\n$fiel = Fiel::create(\n    file_get_contents('certificado.cer'),\n    file_get_contents('llaveprivada.key'),\n    '12345678a'\n);\n\n// verificar que la FIEL sea válida (no sea CSD y sea vigente acorde a la fecha del sistema)\nif (! $fiel-\u003eisValid()) {\n    return;\n}\n\n// creación del web client basado en Guzzle que implementa WebClientInterface\n// para usarlo necesitas instalar guzzlehttp/guzzle, pues no es una dependencia directa\n$webClient = new GuzzleWebClient();\n\n// creación del objeto encargado de crear las solicitudes firmadas usando una FIEL\n$requestBuilder = new FielRequestBuilder($fiel);\n\n// Creación del servicio\n$service = new Service($requestBuilder, $webClient);\n```\n\n### Cliente para consumir los servicios de CFDI de Retenciones\n\nExisten dos tipos de Comprobantes Fiscales Digitales, los regulares (ingresos, egresos, traslados, nóminas y pagos),\ny los CFDI de retenciones e información de pagos (retenciones).\n\nPuede utilizar esta librería para consumir los CFDI de Retenciones. Para lograrlo construya el servicio con\nla especificación de `ServiceEndpoints::retenciones()`.\n\nLos constructores `ServiceEndpoints::cfdi()` y `ServiceEndpoints::retenciones()` agregan automáticamente\nla propiedad `ServiceType` al objeto. Esta propiedad será después utilizada el servicio para especificar\nel valor en la consulta antes de consumirla.\n\n```php\nuse PhpCfdi\\SatWsDescargaMasiva\\RequestBuilder\\RequestBuilderInterface;\nuse PhpCfdi\\SatWsDescargaMasiva\\Service;\nuse PhpCfdi\\SatWsDescargaMasiva\\Shared\\ServiceEndpoints;\nuse PhpCfdi\\SatWsDescargaMasiva\\WebClient\\GuzzleWebClient;\n\n/**\n * @var GuzzleWebClient $webClient Cliente de Guzzle previamente fabricado\n * @var RequestBuilderInterface $requestBuilder Creador de solicitudes, previamente fabricado\n */\n// Creación del servicio\n$service = new Service($requestBuilder, $webClient, null, ServiceEndpoints::retenciones());\n```\n\nAunque no es recomendado, también puedes construir el objeto `ServiceEndpoints` con direcciones URL del\nservicio personalizadas utilizando el constructor del objeto en lugar de los métodos estáticos.\n\n### Realizar una consulta\n\nUna vez creado el servicio, se puede presentar la consulta, si se pudo presentar devolverá el identificador de la solicitud,\ny con este identificador se podrá continuar al servicio de verificación.\n\n```php\n\u003c?php\n\nuse PhpCfdi\\SatWsDescargaMasiva\\Services\\Query\\QueryParameters;\nuse PhpCfdi\\SatWsDescargaMasiva\\Shared\\DateTimePeriod;\n\n// Crear la consulta\n$request = QueryParameters::create(\n    DateTimePeriod::createFromValues('2019-01-13 00:00:00', '2019-01-13 23:59:59'),\n);\n\n// presentar la consulta\n$query = $service-\u003equery($request);\n\n// verificar que el proceso de consulta fue correcto\nif (! $query-\u003egetStatus()-\u003eisAccepted()) {\n    echo \"Fallo al presentar la consulta: {$query-\u003egetStatus()-\u003egetMessage()}\";\n    return;\n}\n\n// el identificador de la consulta está en $query-\u003egetRequestId()\necho \"Se generó la solicitud {$query-\u003egetRequestId()}\", PHP_EOL;\n```\n\n### Parámetros de la consulta\n\n#### Periodo (`DateTimePeriod`)\n\nFecha y hora de inicio y fin de la consulta.\nSi no se especifica crea un periodo del segundo exacto de la creación del objeto.\n\n#### Tipo de descarga (`DownloadType`)\n\nEstablece si la solicitud es de documentos emitidos `DownloadType::issued()` o recibidos `DownloadType::received()`.\nSi no se especifica utiliza el valor de emitidos.\n\n#### Tipo de solicitud (`RequestType`)\n\nEstablece si la solicitud es de Metadatos `RequestType::metadata()` o archivos XML `RequestType::xml()`.\nSi no se especifica utiliza el valor de Metadatos.\n\n#### Tipo de comprobante (`DocumentType`)\n\nFiltra la solicitud por tipo de comprobante. Si no se especifica utiliza no utiliza el filtro.\n\n- Cualquiera: `DocumentType::undefined()` (predeterminado).\n- Ingreso: `DocumentType::ingreso()`.\n- Egreso: `DocumentType::egreso()`.\n- Traslado: `DocumentType::traslado()`.\n- Nómina: `DocumentType::nomina()`.\n- Pago: `DocumentType::pago()`.\n\n#### Tipo de complemento (`ComplementoCfdi` o `ComplementoRetenciones`)\n\nFiltra la solicitud por la existencia de un tipo de complemento dentro del comprobante.\nSi no se especifica utiliza `ComplementoUndefined::undefined()` que excluye el filtro.\n\nHay dos tipos de objetos que satisfacen este parámetro, depende del tipo de comprobante que se está solicitando.\nSi se trata de comprobantes de CFDI Regulares entonces se usa la clase `ComplementoCfdi`.\nSi se trata de CFDI de retenciones e información de pagos entonces se usa la clase `ComplementoRetenciones`.\n\nEstos objetos se pueden crear nombrados (`ComplementoCfdi::leyendasFiscales10()`),\npor constructor (`new ComplementoCfdi('leyendasfisc')`), o bien,\npor el método estático `create` (`ComplementoCfdi::create('leyendasfisc')`).\n\nAdemás, se puede acceder al nombre del complemento utilizando el método `label()`, por ejemplo,\n`echo ComplementoCfdi::leyendasFiscales10()-\u003elabel(); // Leyendas Fiscales 1.0`.\n\nA su vez, este objeto ofrece un método estático `getLabels(): array` para obtener un arreglo con los datos,\nen donde la llave es el identificador del complemento y el valor es el nombre del complemento.\n\n#### Estado del comprobante (`DocumentStatus`)\n\nFiltra la solicitud por el estado de comprobante: Vigente (`DocumentStatus::active()`) y Cancelado (`DocumentStatus::cancelled()`).\nSi no se especifica utiliza `DocumentStatus::undefined()` que excluye el filtro.\n\n#### UUID (`Uuid`)\n\nFiltra la solicitud por UUID.\nPara crear el objeto del filtro hay que usar `Uuid::create('96623061-61fe-49de-b298-c7156476aa8b')`.\nSi no se especifica utiliza `Uuid::empty()` que excluye el filtro.\n\n#### Filtrado a cuenta de terceros (`RfcOnBehalf`)\n\nFiltra la solicitud por el RFC utilizado a cuenta de terceros.\nPara crear el objeto del filtro hay que usar `RfcOnBehalf::create('XXX01010199A')`.\nSi no se especifica utiliza `RfcOnBehalf::empty()` que excluye el filtro.\n\n#### Filtrado por RFC contraparte (`RfcMatch`/`RfcMatches`)\n\nFiltra la solicitud por el RFC en contraparte, es decir, que\nsi la consulta es de emitidos entonces filtrará donde el RFC especificado sea el receptor,\nsi la consulta es de recibidos entonces filtrará donde el RFC especificado sea el emisor.\n\nPara crear el objeto del filtro hay que usar `RfcMatch::create('XXX01010199A')`.\nSi no se especifica utiliza una lista vacía `RfcMatches::create()` que excluye el filtro.\n\n```php\n$rfcMatch = RfcMatch::create('XXX01010199A');\n$parameters = $parameters-\u003ewithRfcMatch();\nvar_dump($rfcMatch === $parameters-\u003egetRfcMatch()); // bool(true)\n```\n\nEl servicio del SAT permite especificar hasta 5 RFC Receptores, al menos así lo establecen en su documentación.\nSin embargo, al tratarse de receptores, solo se puede utilizar en una consulta de documentos emitidos.\nEn el caso de una consulta de documentos recibidos, solo se utilizará el primero de la lista.\n\nPor lo regular utilizará solamente los métodos `QueryParameter::getRfcMatch(): RfcMatch`\ny `QueryParameter::withRfcMatch(RfcMatch $rfcMatch)`.\n\nSin embargo, si fuera necesario especificar el listado de RFC, se puede realizar de la siguiente manera:\n\n```php\n$parameters = $parameters-\u003ewithRfcMatches(\n    RfcMatches::create(\n        RfcMatch::create('AAA010101000'),\n        RfcMatch::create('AAA010101001'),\n        RfcMatch::create('AAA010101002')\n    )\n);\n```\n\nO bien, utilizar una lista de RFC como cadenas de texto:\n\n```php\n$parameters = $parameters-\u003ewithRfcMatches(\n    RfcMatches::createFromValues('AAA010101000', 'AAA010101001', 'AAA010101002')\n);\n```\n\n##### Acerca de `RfcMatches`\n\nEste objeto mantiene una lista de `RfcMatches`, pero con características especiales:\n\n- Los objetos `RfcMatch` *vacíos* o *repetidos* son ignorados, solo se mantienen valores no vacíos únicos.\n- El método `RfcMatch::getFirst()` devuelve siempre el primer elemento, si no existe entonces devuelve uno vacío.\n- La clase `RfcMatch` es *iterable*, se puede hacer `foreach()` sobre los elementos.\n- La clase `RfcMatch` es *contable*, se puede hacer `count()` sobre los elementos.\n\n#### Tipo de servicio (`ServiceType`)\n\nEsta es una propiedad que bien se podría considerar interna y no necesitas especificarla en la consulta.\nPor defecto está no definida y con el valor `null`. Se puede conocer si la propiedad ha sido definida\ncon la propiedad `hasServiceType(): bool` y cambiar con `withServiceType(ServiceType): self`.\n\nNo se recomienda definir esta propiedad y dejar que el servicio establezca el valor correcto\nsegún a donde esté apuntando el servicio.\n\nCuando se ejecuta una consulta, el servicio (`Service`) automáticamente define esta propiedad si es que\nno está definida estableciéndole el mismo valor que está definido en el objeto `ServiceEndpoints`.\nSi esta propiedad ya estaba definida, y su valor no es el mismo que el definido en el objeto `ServiceEndpoints`\nentonces se genera una `LogicException`.\n\n#### Ejemplo de especificación de parámetros\n\nEn el siguiente ejemplo, se crea una consulta sin parámetros y posteriormente se van modificando.\nLos métodos no cambian la propiedad del objeto (no son `set*`), lo que hacen es crear una nueva\ninstancia de la consulta con los nuevos valores (son `with*`).\n\nPuede que los cambios del ejemplo no sean lógicos, es solo para ilustrar cómo se establecen los valores:\n\n- Un periodo específico de `2019-01-13 00:00:00` a `2019-01-13 23:59:59` (inclusive).\n- Sobre los documentos recibidos.\n- Solicitando los archivos XML.\n- Filtrando por documentos de tipo ingreso.\n- Filtrando por los que tengan el complemento de leyendas fiscales.\n- Filtrando por únicamente documentos vigentes (excluye cancelados).\n- Filtrando por el RFC a cuenta de terceros `XXX01010199A`.\n- Filtrando por el RFC contraparte `MAG041126GT8`. Como se solicitan recibidos, entonces son los emidos por ese RFC.\n- Filtrando por el UUID `96623061-61fe-49de-b298-c7156476aa8b`.\n\n```php\n\u003c?php\n\nuse PhpCfdi\\SatWsDescargaMasiva\\Services\\Query\\QueryParameters;\nuse PhpCfdi\\SatWsDescargaMasiva\\Shared\\ComplementoCfdi;\nuse PhpCfdi\\SatWsDescargaMasiva\\Shared\\DateTimePeriod;\nuse PhpCfdi\\SatWsDescargaMasiva\\Shared\\DocumentStatus;\nuse PhpCfdi\\SatWsDescargaMasiva\\Shared\\DocumentType;\nuse PhpCfdi\\SatWsDescargaMasiva\\Shared\\DownloadType;\nuse PhpCfdi\\SatWsDescargaMasiva\\Shared\\RequestType;\nuse PhpCfdi\\SatWsDescargaMasiva\\Shared\\RfcMatch;\nuse PhpCfdi\\SatWsDescargaMasiva\\Shared\\RfcOnBehalf;\nuse PhpCfdi\\SatWsDescargaMasiva\\Shared\\Uuid;\n\n$query = QueryParameters::create()\n    -\u003ewithPeriod(DateTimePeriod::createFromValues('2019-01-13 00:00:00', '2019-01-13 23:59:59'))\n    -\u003ewithDownloadType(DownloadType::received())\n    -\u003ewithRequestType(RequestType::xml())\n    -\u003ewithDocumentType(DocumentType::ingreso())\n    -\u003ewithComplement(ComplementoCfdi::leyendasFiscales10())\n    -\u003ewithDocumentStatus(DocumentStatus::active())\n    -\u003ewithRfcOnBehalf(RfcOnBehalf::create('XXX01010199A'))\n    -\u003ewithRfcMatch(RfcMatch::create('MAG041126GT8'))\n    -\u003ewithUuid(Uuid::create('96623061-61fe-49de-b298-c7156476aa8b'))\n;\n```\n\n#### Ejemplo de consulta por UUID\n\nEn este caso se especifica solamente el UUID a consultar, en el ejemplo es `96623061-61fe-49de-b298-c7156476aa8b`.\n\nNota: **Todos los demás argumentos de la consulta son ignorados**.\n\n```php\n\u003c?php\n\nuse PhpCfdi\\SatWsDescargaMasiva\\Services\\Query\\QueryParameters;\nuse PhpCfdi\\SatWsDescargaMasiva\\Shared\\Uuid;\n\n$query = QueryParameters::create()\n    -\u003ewithUuid(Uuid::create('96623061-61fe-49de-b298-c7156476aa8b'))\n;\n```\n\n#### Prevalidación de una consulta\n\nHay algunos casos que seguramente resultarán en un error al momento de presentar la consulta al SAT.\nPara prevenir esta situación *opcionalmente* se puede validar la consulta antes de presentarla.\nEstos errores son devueltos en un listado de cadenas de caracteres.\n\n```php\n\u003c?php\n\nuse PhpCfdi\\SatWsDescargaMasiva\\Services\\Query\\QueryParameters;\nuse PhpCfdi\\SatWsDescargaMasiva\\Shared\\DocumentStatus;\nuse PhpCfdi\\SatWsDescargaMasiva\\Shared\\DocumentType;\nuse PhpCfdi\\SatWsDescargaMasiva\\Shared\\Uuid;\n\n$query = QueryParameters::create()\n    -\u003ewithUuid(Uuid::create('96623061-61fe-49de-b298-c7156476aa8b'))\n    -\u003ewithDocumentType(DocumentType::nomina())\n    -\u003ewithDocumentStatus(DocumentStatus::active())\n;\n\n// obtener el listado de errores\n$errors = $query-\u003evalidate();\nif ([] !== $errors) { // si hay errores\n    echo 'Errores de consulta: ', PHP_EOL;\n    foreach ($errors as $error) {\n        echo '  - ', $error, PHP_EOL;\n    }\n}\n```\n\n### Verificar una consulta\n\nLa verificación depende de que la consulta haya sido aceptada.\n\n```php\n\u003c?php\n\nuse PhpCfdi\\SatWsDescargaMasiva\\Service;\n\n/**\n * @var Service $service Objeto de ayuda de consumo de servicio, previamente fabricado\n * @var string $requestId Identificador generado al presentar la consulta, previamente fabricado\n */\n\n// consultar el servicio de verificación\n$verify = $service-\u003everify($requestId);\n\n// revisar que el proceso de verificación fue correcto\nif (! $verify-\u003egetStatus()-\u003eisAccepted()) {\n    echo \"Fallo al verificar la consulta {$requestId}: {$verify-\u003egetStatus()-\u003egetMessage()}\";\n    return;\n}\n\n// revisar que la consulta no haya sido rechazada\nif (! $verify-\u003egetCodeRequest()-\u003eisAccepted()) {\n    echo \"La solicitud {$requestId} fue rechazada: {$verify-\u003egetCodeRequest()-\u003egetMessage()}\", PHP_EOL;\n    return;\n}\n\n// revisar el progreso de la generación de los paquetes\n$statusRequest = $verify-\u003egetStatusRequest();\nif ($statusRequest-\u003eisExpired() || $statusRequest-\u003eisFailure() || $statusRequest-\u003eisRejected()) {\n    echo \"La solicitud {$requestId} no se puede completar\", PHP_EOL;\n    return;\n}\nif ($statusRequest-\u003eisInProgress() || $statusRequest-\u003eisAccepted()) {\n    echo \"La solicitud {$requestId} se está procesando\", PHP_EOL;\n    return;\n}\nif ($statusRequest-\u003eisFinished()) {\n    echo \"La solicitud {$requestId} está lista\", PHP_EOL;\n}\n\necho \"Se encontraron {$verify-\u003ecountPackages()} paquetes\", PHP_EOL;\nforeach ($verify-\u003egetPackagesIds() as $packageId) {\n    echo \" \u003e {$packageId}\", PHP_EOL;\n}\n```\n\n### Descargar los paquetes de la consulta\n\nLa descarga de los paquetes depende de que la consulta haya sido correctamente verificada.\n\nUna consulta genera un identificador de la solicitud,\nla verificación retorna **uno o varios** identificadores de paquetes.\nNecesitas descargar todos y cada uno de los paquetes para tener la información completa de la consulta.\n\n```php\n\u003c?php\n\nuse PhpCfdi\\SatWsDescargaMasiva\\Service;\n\n/**\n * @var Service $service Objeto de ayuda de consumo de servicio, previamente fabricado\n * @var string[] $packagesIds Listado de identificadores de paquetes generado en la verificación, previamente fabricado\n */\n\n// consultar el servicio de verificación\nforeach($packagesIds as $packageId) {\n    $download = $service-\u003edownload($packageId);\n    if (! $download-\u003egetStatus()-\u003eisAccepted()) {\n        echo \"El paquete {$packageId} no se ha podido descargar: {$download-\u003egetStatus()-\u003egetMessage()}\", PHP_EOL;\n        continue;\n    }\n    $zipfile = \"$packageId.zip\";\n    file_put_contents($zipfile, $download-\u003egetPackageContent());\n    echo \"El paquete {$packageId} se ha almacenado\", PHP_EOL;\n}\n```\n\n### Lectura de paquetes\n\nLos paquetes de Metadata y CFDI se pueden leer con las clases `MetadataPackageReader` y `CfdiPackageReader` respectivamente.\nPara fabricar los objetos, se pueden usar sus métodos `createFromFile` para crearlo a partir de un archivo existente\no `createFromContents` para crearlo a partir del contenido del archivo en memoria.\n\nCada paquete puede contener uno o más archivos internos. Cada paquete se lee individualmente.\n\n#### Lectura de paquetes de tipo Metadata\n\n```php\n\u003c?php\nuse PhpCfdi\\SatWsDescargaMasiva\\PackageReader\\Exceptions\\OpenZipFileException;\nuse PhpCfdi\\SatWsDescargaMasiva\\PackageReader\\MetadataPackageReader;\n\n/**\n * @var string $zipfile Contiene la ruta al archivo de paquete de Metadata\n */\n\n// abrir el archivo de Metadata\ntry {\n    $metadataReader = MetadataPackageReader::createFromFile($zipfile);\n} catch (OpenZipFileException $exception) {\n    echo $exception-\u003egetMessage(), PHP_EOL;\n    return;\n}\n\n// leer todos los registros de metadata dentro de todos los archivos del archivo ZIP\nforeach ($metadataReader-\u003emetadata() as $uuid =\u003e $metadata) {\n    echo $metadata-\u003euuid, ': ', $metadata-\u003efechaEmision, PHP_EOL;\n}\n```\n\n#### Lectura de paquetes de tipo CFDI\n\n```php\n\u003c?php\nuse PhpCfdi\\SatWsDescargaMasiva\\PackageReader\\Exceptions\\OpenZipFileException;\nuse PhpCfdi\\SatWsDescargaMasiva\\PackageReader\\CfdiPackageReader;\n\n/**\n * @var string $zipfile Contiene la ruta al archivo de paquete de archivos ZIP\n */\ntry {\n    $cfdiReader = CfdiPackageReader::createFromFile($zipfile);\n} catch (OpenZipFileException $exception) {\n    echo $exception-\u003egetMessage(), PHP_EOL;\n    return;\n}\n\n// leer todos los CFDI dentro del archivo ZIP con el UUID como llave\nforeach ($cfdiReader-\u003ecfdis() as $uuid =\u003e $content) {\n    file_put_contents(\"cfdis/$uuid.xml\", $content);\n}\n```\n\n## Información técnica\n\n### Acerca de la interfaz `RequestBuilderInterface`\n\nEl Servicio Web del SAT de Descarga Masiva requiere comunicación SOAP especial, con autenticación\ny mensajes firmados. Generar estos mensajes requiere de gran detalle porque si el mensaje contiene\nerrores será inmediatamente rechazado.\n\nLa firma de estos mensajes es con la FIEL, así que se puede utilizar la clase `FielRequestBuilder` que\njunto con la clase `Fiel` y la librería [phpcfdi/credentials](https://github.com/phpcfdi/credentials)\nhacen la combinación adecuada para firmar los mensajes.\n\nSin embargo, existen escenarios distribuidos donde lo mejor sería contar con la creación de estos mensajes\nfirmados en un lugar externo, de esta forma la FIEL (la llave privada y contraseña) no se necesita exponer\nal exterior. Para estos (u otros) escenarios, es posible crear una implementación de `RequestBuilderInterface`\nque contenga la lógica adecuada y entregue los mensajes firmados necesarios para la comunicación. \n\n### Acerca de la interfaz `WebClientInterface`\n\nPara hacer esta librería compatible con diferentes formas de comunicación se utiliza una interfaz de cliente HTTP.\nTú *puedes* crear tu implementación para poderla utilizar.\n \nSi lo prefieres -como en el ejemplo de uso- podrías instalar Guzzle `composer require guzzlehttp/guzzle` y usar la clase\n[`GuzzleWebClient`](https://github.com/phpcfdi/sat-ws-descarga-masiva/blob/main/src/WebClient/GuzzleWebClient.php).\n\n### Recomendación de fábrica del servicio\n\nTe recomendamos configurar el framework de tu aplicación (Dependency Injection Container) o crear una clase que\nfabrique los objetos `Service`, `RequestBuilder` y `WebClient`, usando tus propias configuraciones de `Fiel`\nen caso de que tengas disponible el certificado, llave privada y contraseña.\n\n### Manejo de excepciones\n\nAl trabajar con el lector de paquetes (PackageReader) o con la comunicación HTTP con el servidor\nweb set SAT (WebClient), la librería puede lanzar excepciones que puedes atrapar y analizar, ya\nsea en el momento de implementación o para personalizar los mensajes de error.\n\n- [Documentación específica de excepciones de `phpcfd/sat-ws-descarga-masiva`](docs/Excepciones.md).\n\n## Acerca del Servicio Web de Descarga Masiva de CFDI y Retenciones\n\nEl servicio se compone de 4 partes:\n\n1. Autenticación: Esto se hace con tu FIEL y la librería oculta la lógica de obtener y usar el Token.\n2. Solicitud: Presentar una solicitud incluyendo la fecha de inicio, fecha de fin, tipo de solicitud\n   emitidas/recibidas y tipo de información solicitada (cfdi o metadata).\n3. Verificación: pregunta al SAT si ya tiene disponible la solicitud.\n4. Descargar los paquetes emitidos por la solicitud.\n\nUna forma burda de entenderlo es: imagina que el servicio del SAT se compone de tres ventanillas con tres\npersonas diferentes atendiendo cada una de estas ventanillas.\n\n* En la primera vas y presentas una solicitud de información. Te firman de recibido, pero eso no significa que tu\ninformación esté lista, solo que han recibido tu solicitud.\n\n* En la segunda ventanilla preguntas por tu número de solicitud y te responden que aún no tienen lista la solicitud,\nregresas después y te dicen que aún no está lista, hasta que finalmente te dicen que ya está completada, y te piden\npasar a otra ventanilla por las cajas con tu información.\n\n* En la última ventanilla llegas y pides cada una de las cajas, una a la vez, te las entregan y te las llevas.\nSi perdiste tu caja y regresaste varios días después y pides la caja, puede que ya no esté disponible.\nSi le pides muchas veces una caja puede que te digan que dejes de estar pidiendo la misma caja y haces enojar\nal funcionario del SAT y no te la da más.\n\n* Todo esto sucede con un máximo de seguridad, cada vez que hablas con un funcionario te pide que le enseñes tu permiso\ny si no lo tienes o ya está vencido (duran apenas unos minutos) te mandan con la persona de seguridad para que le\ndemuestres que eres tú y te extienda un nuevo permiso.\n\n### Información oficial\n\nA la fecha de liberación del *Servicio web de descarga masiva de terceros para CFDI y CFDI de Retenciones* (2025-05-30)\nel SAT no ha publicado información oficial en su página. Esta información se ha recopilado por miembros de la comunidad\nde diferentes fuentes.\n\n- Información oficial del SAT (apartado de *Documentos relacionados*):\n  \u003chttps://www.sat.gob.mx/portal/public/tramites/factura-electronica\u003e\n- Solicitud de descargas para CFDI y retenciones:\n  \u003chttps://ampocdevbuk01a.s3.us-east-1.amazonaws.com/1_WS_Solicitud_Descarga_Masiva_V1_5_VF_89183c42e9.pdf\u003e\n- Verificación de descargas de solicitudes exitosas:\n  \u003chttps://ampocdevbuk01a.s3.us-east-1.amazonaws.com/2_WS_Verificacion_de_Descarga_Masiva_V1_5_VF_5e53cc2bb5.pdf\u003e\n- Descarga de solicitudes exitosas:\n  \u003chttps://ampocdevbuk01a.s3.us-east-1.amazonaws.com/3_WS_Descarga_de_Solicitudes_Exitosas_V1_5_VF_74f66e46ec.pdf\u003e\n\nNotas importantes del web service:\n\n- Podrás recuperar hasta 200 mil registros por petición y hasta 1,000,000 en metadata.\n- No existe limitante en cuanto al número de solicitudes siempre que no se descargue en más de dos ocasiones un XML.\n- No se pueden consultar comprobantes a un periodo máximo de cinco años hacia atrás.\n- No se pueden consultar comprobantes a un periodo máximo de seis ejercicios, incluyendo el actual.\n\n### Notas de uso\n\n- No se aplica la restricción de la documentación oficial: *que no se descargue en más de dos ocasiones un XML*.\n\nSe ha encontrado que la regla relacionada con las descargas de tipo CFDI no se aplica en la forma como está redactada.\nSin embargo, se ha encontrado que la regla que sí aplica es: *no solicitar en más de 2 ocasiones el mismo periodo*.\nCuando esto ocurre, el proceso de solicitud devuelve el mensaje *\"5002: Se han agotado las solicitudes de por vida\"*.\n\nRecuerda que, si se cambia la fecha inicial o final en al menos un segundo ya se trata de otro periodo,\npor lo que si te encuentras en este problema podrías solucionarlo de esta forma.\n\nEn consultas del tipo Metadata no se aplica la limitante mencionada anteriormente, por ello es recomendable\nhacer las pruebas de implementación con este tipo de consulta.\n\n- Tiempo de respuesta entre la presentación de la consulta y su verificación exitosa.\n\nNo se ha podido encontrar una constante para suponer el tiempo que puede tardar una consulta en regresar un estado\nde verificación exitosa y que los paquetes estén listos para descargarse.\n\nEn nuestra experiencia, entre más grande el periodo y más consultas se presenten más lenta es la respuesta,\ny puede ser desde minutos a horas. Por lo general es raro que excedan 24 horas.\nSin embargo, varios usuarios han experimentado casos raros (posiblemente por problemas en el SAT) en donde las\nsolicitudes han llegado a tardar hasta 72 horas para ser completadas.\n\n#### Cambios en el webservice versión 1.5 (2025-05-30)\n\n- Ya no es posible consultar un instante.\n\nEl SAT ha puesto la restricción de que la fecha de inicio de la consulta debe ser menor (y no igual)\na la fecha final de la consulta. Por lo que es imposible consultar un solo instante.\nEs obligatorio ahora consultar como mínimo un intervalo de dos segundos.\n\n- Cambia el límite inferior en el periodo de consulta.\n\nEl SAT ha cambiado su validación del límite inferior en la fecha consultada,\nahora el límite inferior es la fecha actual seis años atrás sin tiempo.\n\nPor ejemplo, si la fecha actual fuera `2025-01-13 14:15:16`, entonces el límite inferior sería `2019-01-13 00:00:00`.\nSi se solicita `2019-01-12 23:59:59` como fecha de inicio del periodo entonces la consulta falla.\n\n- Falla al solicitar Recibidos XML que incluyan cancelados.\n\nEl SAT en su nueva versión del webservice ha puesto una nueva validación en la que,\nal presentar una solicitud de documentos recibidos (`DownloadType::received()`)\ny el tipo de paquete solicitado sea XML (`DownloadType::xml()`),\nfallará a menos que se especifique que se solicitan los documentos con estado activo (`DocumentStatus::active()`).\n\nPara corregir este problema se recomienda que implementes algo como el siguiente ejemplo:\n\n```php\nuse PhpCfdi\\SatWsDescargaMasiva\\Services\\Query\\QueryParameters;\nuse PhpCfdi\\SatWsDescargaMasiva\\Shared\\DocumentStatus;\n\n/**\n * @var QueryParameters $query Consulta elaborada previamente, antes de presentarla.  \n */\n\nif ($query-\u003egetDownloadType()-\u003eisReceived() \u0026\u0026 $query-\u003egetRequestType()-\u003eisXml()) {\n    $query = $query-\u003ewithDocumentStatus(DocumentStatus::active());\n}\n```\n\n## Compatibilidad\n\nEsta librería se mantendrá compatible con al menos la versión con\n[soporte activo de PHP](https://www.php.net/supported-versions.php) más reciente.\n\nTambién utilizamos [Versionado Semántico 2.0.0](https://semver.org/lang/es/)\npor lo que puedes usar esta librería sin temor a romper tu aplicación.\n\n### Actualizaciones\n\n- [Guía de actualización de versión 0.3 a 0.4](docs/UPGRADE_0.3_0.4.md).\n- [Guía de actualización de versión 0.4 a 0.5](docs/UPGRADE_0.4_0.5.md).\n- [Guía de actualización de versión 0.5 a 1.0](docs/UPGRADE_0.5_1.0.md).\n\n## Contribuciones\n\nLas contribuciones son bienvenidas. Por favor lee [CONTRIBUTING][] para más detalles\ny recuerda revisar el archivo de tareas pendientes [TODO][] y el archivo [CHANGELOG][].\n\n## Copyright and License\n\nThe `phpcfdi/sat-ws-descarga-masiva` library is copyright © [PhpCfdi](https://www.phpcfdi.com)\nand licensed for use under the MIT License (MIT). Please see [LICENSE][] for more information.\n\n[contributing]: https://github.com/phpcfdi/sat-ws-descarga-masiva/blob/main/CONTRIBUTING.md\n[changelog]: https://github.com/phpcfdi/sat-ws-descarga-masiva/blob/main/docs/CHANGELOG.md\n[todo]: https://github.com/phpcfdi/sat-ws-descarga-masiva/blob/main/docs/TODO.md\n\n[source]: https://github.com/phpcfdi/sat-ws-descarga-masiva\n[php-version]: https://packagist.org/packages/phpcfdi/sat-ws-descarga-masiva\n[discord]: https://discord.gg/aFGYXvX\n[release]: https://github.com/phpcfdi/sat-ws-descarga-masiva/releases\n[license]: https://github.com/phpcfdi/sat-ws-descarga-masiva/blob/main/LICENSE\n[build]: https://github.com/phpcfdi/sat-ws-descarga-masiva/actions/workflows/build.yml?query=branch:main\n[reliability]:https://sonarcloud.io/component_measures?id=phpcfdi_sat-ws-descarga-masiva\u0026metric=Reliability\n[maintainability]: https://sonarcloud.io/component_measures?id=phpcfdi_sat-ws-descarga-masiva\u0026metric=Maintainability\n[coverage]: https://sonarcloud.io/component_measures?id=phpcfdi_sat-ws-descarga-masiva\u0026metric=Coverage\n[violations]: https://sonarcloud.io/project/issues?id=phpcfdi_sat-ws-descarga-masiva\u0026resolved=false\n[downloads]: https://packagist.org/packages/phpcfdi/sat-ws-descarga-masiva\n\n[badge-source]: https://img.shields.io/badge/source-phpcfdi/sat--ws--descarga--masiva-blue?logo=github\n[badge-discord]: https://img.shields.io/discord/459860554090283019?logo=discord\n[badge-php-version]: https://img.shields.io/packagist/php-v/phpcfdi/sat-ws-descarga-masiva?logo=php\n[badge-release]: https://img.shields.io/github/release/phpcfdi/sat-ws-descarga-masiva?logo=git\n[badge-license]: https://img.shields.io/github/license/phpcfdi/sat-ws-descarga-masiva?logo=open-source-initiative\n[badge-build]: https://img.shields.io/github/actions/workflow/status/phpcfdi/sat-ws-descarga-masiva/build.yml?branch=main\u0026logo=github-actions\n[badge-reliability]: https://sonarcloud.io/api/project_badges/measure?project=phpcfdi_sat-ws-descarga-masiva\u0026metric=reliability_rating\n[badge-maintainability]: https://sonarcloud.io/api/project_badges/measure?project=phpcfdi_sat-ws-descarga-masiva\u0026metric=sqale_rating\n[badge-coverage]: https://img.shields.io/sonar/coverage/phpcfdi_sat-ws-descarga-masiva/main?logo=sonarcloud\u0026server=https%3A%2F%2Fsonarcloud.io\n[badge-violations]: https://img.shields.io/sonar/violations/phpcfdi_sat-ws-descarga-masiva/main?format=long\u0026logo=sonarcloud\u0026server=https%3A%2F%2Fsonarcloud.io\n[badge-downloads]: https://img.shields.io/packagist/dt/phpcfdi/sat-ws-descarga-masiva?logo=packagist\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fphpcfdi%2Fsat-ws-descarga-masiva","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fphpcfdi%2Fsat-ws-descarga-masiva","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fphpcfdi%2Fsat-ws-descarga-masiva/lists"}