Skip to content

Latest commit

 

History

History
180 lines (136 loc) · 83.9 KB

CHANGELOG.md

File metadata and controls

180 lines (136 loc) · 83.9 KB

Changelog

Cambios del SDK y notas de comportamiento del API de FiscalAPI que afectan a quien usa el SDK.

[4.0.474] - 2026-11-07

Cambios incompatibles (BREAKING)

  • Cambio del API, no del SDK: cancelar factura (InvoiceService.cancel) frente a un rechazo del SAT responde 422 con isSucceeded() en false. Antes respondía 200 con isSucceeded() en true y el código 203 en getData().getInvoiceUuids(). El SDK no lanza excepción.
  • getData() sigue con el acuse (InvoiceCancellationResponse) y getErrorData() con el JSON crudo: solo cambian el status (422) y isSucceeded(). El código del SAT está en getDetails().
  • Migración: trate el 422 como rechazo; quien filtraba por getHttpStatusCode() 200 o por isSucceeded() cambia de rama.
  • Las validaciones previas al PAC responden 400: UUID no canónico, sustituta inválida con motivo 01, y cancelar por id una factura emitida por valores cuando el tenant no tiene una persona con ese RFC y CSD vigente o tiene más de una.
  • Si el PAC no trae el código del SAT, la respuesta es 502.
  • Una factura rechazada se queda vigente; ya no queda en «Cancelando».
  • Cancelar por valores un UUID que FiscalAPI no tiene ya no responde 404: responde el resultado del PAC con su acuse y no toca ninguna factura.

Cancelación de facturas y webhooks

  • WebhookEventData.getReason() (nuevo, String): el motivo de la transición. Todo evento lo trae; es null salvo en invoice.cancellation_rejected, donde dice por qué no procedió la cancelación: rejectedByRecipient, expired, notRequested o notCancelable. Es texto libre: pueden aparecer valores nuevos. Cambio aditivo: nada existente cambia ni deja de compilar.
  • InvoiceService.cancel frente a un rechazo del SAT: el API responde 422 y el SDK no lanza. isSucceeded() es false, getMessage() es el mensaje genérico, getDetails() trae SAT <código>: <descripción> y el acuse llega tipado en getData() (InvoiceCancellationResponse: getBase64CancellationAcknowledgement() e getInvoiceUuids() con el código del SAT por UUID) y crudo en getErrorData(). El SDK no cambia de código: el cambio es del API (ver Cambios incompatibles).

[4.0.469] - 2026-10-30

Seguridad

  • Jackson sube de 2.14.2 a 2.18.12 (vía jackson-bom), que corrige avisos de seguridad de jackson-databind y jackson-core (p. ej. GHSA-h46c-h94j-95f3). Sin cambios de API.

[4.0.468] - 2026-10-28

InvoicePayment

  • getForeignBankName() y setForeignBankName(String) (foreignBankName, opcional): nombre del banco ordenante extranjero, el atributo NomBancoOrdExt del complemento de pago. El SAT lo exige cuando sourceBankTin es XEXX010101000 (CRP20223); antes el SDK no podía enviarlo. Sin asignarlo no se envía, así que el código existente manda el mismo JSON.

[4.0.462] - 2026-10-22

Agregado

  • Llave de idempotencia opcional con el header Idempotency-Key en la creación de facturas y en la transferencia de timbres:
    • getInvoiceService().create(invoice, idempotencyKey) (POST /api/v4/invoices) y getStampService().transferStamps(params, idempotencyKey) (POST /api/v4/stamps). Con una llave de idempotencia distinta de null envían el header; create(invoice) y transferStamps(params) no cambian y no lo envían. La llave de idempotencia admite hasta 64 caracteres ASCII visibles, sin espacios.
    • Repetir la misma petición con la misma llave de idempotencia dentro de 24 horas devuelve la respuesta original. Otra petición con la misma llave de idempotencia responde 422, y una petición original todavía en proceso responde 409. El SDK no genera llaves de idempotencia ni reintenta por su cuenta.
    • Quien implementa IInvoiceService o IStampService (p. ej. un doble de pruebas) debe agregar las sobrecargas nuevas; quien usa el cliente del SDK no cambia nada.
  • Crear ticket (getTicketService().create()) usa el mismo mecanismo de idempotencia de la API: una repetición con la misma llave de idempotencia devuelve la respuesta original (201) con Idempotent-Replayed: true en lugar del 200 con el ticket actual; la llave de idempotencia vence a las 24 h y es por persona. La firma del método no cambia.

[4.0.450] - 2026-10-14

Webhooks

  • Webhook.verify(headers, body, secret) y Webhook.verify(headers, body, secret, tolerance) (paquete com.fiscalapi.webhooks) verifican una entrega de webhook de FiscalAPI y devuelven su WebhookEvent: validan la firma Standard Webhooks (HMAC-SHA256 sobre webhook-id.webhook-timestamp.cuerpo con el secreto whsec_…, comparada en tiempo constante) y que webhook-timestamp no difiera del reloj del receptor más que la tolerancia (java.time.Duration), hacia atrás o hacia adelante (por omisión Webhook.DEFAULT_TOLERANCE, 5 minutos). Aceptan cualquier firma v1 válida del header webhook-signature (varias, separadas por espacio) y el secreto con o sin el prefijo whsec_. body es el texto (String) o los bytes (byte[]) del cuerpo; con byte[] se lee como UTF-8 estricto. Las sobrecargas verify(headers, body, List<String> secrets) y verify(headers, body, List<String> secrets, tolerance) aceptan varios secretos para la rotación (FiscalAPI firma con el nuevo y el anterior durante 24 h). headers es un Map<String, String> o un Map<String, List<String>> (servlet, @RequestHeader o MultiValueMap de Spring), y los nombres se comparan sin distinguir mayúsculas.
    • Opera sobre el cuerpo crudo, leído como texto UTF-8 antes de deserializarlo: un cuerpo reserializado no verifica. Ejemplo para Spring en el README (sección 9), con el cuerpo como @RequestBody byte[] pasado directo a verify.
    • Una entrega que no se puede tratar como auténtica (falta un header, ninguna firma coincide, timestamp fuera de la tolerancia, un cuerpo que no es texto UTF-8 válido o un cuerpo firmado que no es un evento JSON) lanza WebhookVerificationException (no verificada, extiende RuntimeException); un secreto que no es base64 canónico (por ejemplo, truncado al copiarlo o con espacios), una lista de secretos vacía, una tolerancia negativa o un argumento null lanzan IllegalArgumentException (error de configuración del receptor), que en la sobrecarga de varios secretos indica la posición del secreto y nunca su valor.
    • El evento se lee con un lector de Jackson propio del SDK: no usa el ObjectMapper del cliente HTTP ni uno de la aplicación, acepta campos desconocidos, normaliza createdAt a UTC sin importar la zona de la JVM y rechaza contenido después del evento.
  • Modelo WebhookEvent (paquete com.fiscalapi.models.webhooks), el mismo sobre para todo evento: getId() (igual al header webhook-id, se repite en cada reintento), getType(), getCreatedAt() (OffsetDateTime en UTC), getPersonId() (null si el recurso no tiene persona) y getData() (WebhookEventData) con getRelatedObject() (WebhookRelatedObject: getId(), getType(), getUrl()), getStatus() y getPreviousStatus(). getRelatedObject().getUrl() es la ruta relativa del GET v4 del recurso (por ejemplo /api/v4/invoices/{id}); el recurso se lee con el servicio de su tipo y getRelatedObject().getId().
  • getType() es texto y no un enum: las constantes de WebhookEventTypes (INVOICE_CANCELLATION_REQUESTED, INVOICE_CANCELED, INVOICE_CANCELLATION_REJECTED, DOWNLOAD_REQUEST_COMPLETED, DOWNLOAD_REQUEST_ABANDONED, DOWNLOAD_RULE_SUSPENDED y WEBHOOK_TEST) son una ayuda para comparar, y un tipo o un campo que el SDK no conoce llega sin error. WEBHOOK_TEST (webhook.test) no se suscribe: llega solo al pulsar "Enviar prueba" (POST /api/v4/webhooks/{id}/test), con relatedObject.type webhook y relatedObject.url /api/v4/webhooks/{id}, sin persona ni estados.
  • Cambio aditivo: nada existente cambia ni deja de compilar, y no hay dependencias nuevas.

[4.0.439] - 2026-10-12

Enum CreditType

  • Tipo de crédito TICKET (3) para transferir y leer créditos de ticket: getStampService().transferStamps() con setCreditType(CreditType.TICKET) los transfiere con las mismas reglas que los timbres, y las transacciones (getStampService().getList(), getStampService().getById()) los devuelven como CreditType.TICKET. El saldo de créditos de ticket se lee en getBalances() de la persona (el CreditBalance con getCreditType() igual a CreditType.TICKET.getValue()); un tipo ausente tiene saldo 0. Los valores existentes no cambian (STAMP 1, VALIDATION 2).

Modelo StampTransaction

  • Las transacciones con un tipo de crédito, tipo o estatus desconocido conservan su número en get*Value() y al serializar: los nuevos getCreditTypeValue(), getTransactionTypeValue() y getTransactionStatusValue() devuelven el entero que envió la API (Integer), y toString() lo vuelve a escribir tal cual. Antes el número se perdía. getCreditType(), getTransactionType() y getTransactionStatus() no cambian: con un número que el SDK no conoce devuelven null, como antes.

Servicio de tickets (Auto CFDI)

  • Servicio de tickets (Auto CFDI): tickets.create (con Idempotency-Key opcional), getList con filtros, getById, getCode y cancel, en getTicketService() (ITicketService/TicketService):
    • create(ticket) y create(ticket, idempotencyKey): POST /api/v4/tickets; cobra 1 crédito de ticket y devuelve TicketIngestionResult (id, ticketCode, qrUrl, status, selfInvoiceEligible, eligibleUntil y los importes calculados). Con idempotencyKey distinto de null envía el header Idempotency-Key (sin ella no lo envía): repetir la key con el mismo cuerpo responde 200 con el ticket original y sin cobrar. Un folio y serie ya activos del emisor responden 409 y un total que no cuadra 422, como ApiResponse fallida con el texto de la API en getDetails() y su data en getErrorData().
    • getList(TicketListFilters): GET /api/v4/tickets con status (List<TicketStatus>; se repite la llave status), personId, details (con true envía details=true: cada ticket trae sus conceptos con impuestos, sin la historia; null o false no se envían), globalInvoiceId (factura global que ampara los tickets), from/to (LocalDate, fecha civil de México), pageNumber y pageSize; los filtros en null no se envían.
    • getById(id, details): con details trae items (con itemTaxes) y events. getCode(id) devuelve TicketCodeResult (ticketCode, qrUrl) para reimprimir. cancel(id) usa DELETE (solo desde pending; el crédito no se devuelve).
  • Modelos nuevos en com.fiscalapi.models.tickets: Ticket, TicketIssuer, TicketItem, TicketItemTax, TicketEvent, TicketIngestionResult, TicketCodeResult y TicketListFilters. paymentDate es LocalDateTime en hora civil de México y viaja como yyyy-MM-ddTHH:mm:ss; los importes (BigDecimal) se envían como texto con su escala (constrúyalos desde un String).
  • Enums TicketStatus, TicketEventType y TicketActorType: los modelos guardan el texto de la API; getStatus(), getType() y getActorType() devuelven null con un valor que el SDK no conoce, y getStatusValue(), getTypeValue() y getActorTypeValue() conservan el texto.
  • IFiscalApiClient agrega getTicketService(). Quien implementa su propia versión de IFiscalApiClient (p. ej. un doble de pruebas) debe agregar getTicketService(); quien usa el cliente del SDK no cambia nada.
  • IFiscalApiHttpClient agrega la sobrecarga post(url, body, responseType, headers) para headers propios de la petición (la usa Idempotency-Key). Quien implementa su propia versión de IFiscalApiHttpClient (p. ej. un doble de pruebas) debe agregar post(url, body, responseType, headers); quien usa el cliente del SDK no cambia nada.
  • Ejemplo: examples/EjemplosTickets.java.

Modelo InvoiceItem

  • ticketId en el concepto de la factura para ligar la factura global a los tickets (Auto CFDI); se omite si es nulo. En Java es InvoiceItem.getTicketId()/setTicketId(String), con @JsonInclude(NON_NULL) en el campo: en null no viaja en el JSON, aunque el ObjectMapper incluya los nulos. Un 409 por tickets no disponibles trae unavailableTickets (ticketId, status) en getErrorData(). Ejemplo: examples/EjemplosFacturaGlobalTickets.java.

ApiResponse

  • getErrorData() (JsonNode de Jackson): nuevo miembro (aditivo) con el data crudo de una respuesta de error, tal como lo envía la API: por ejemplo existingTicketId en el 409 de la ingesta de tickets (response.getErrorData().get("existingTicketId").asText()) o expectedTotal y computedTotal en su 422. Es null en las respuestas exitosas y en los errores sin data. getData(), getMessage(), getDetails(), getHttpStatusCode() y getTraceIdentifier() no cambian.

[4.0.433] - 2026-10-10

Cambios incompatibles (BREAKING)

  • Payroll.daysPaid pasa de int a BigDecimal (getDaysPaid() devuelve BigDecimal y setDaysPaid(BigDecimal) lo recibe): el API y el SAT admiten días pagados fraccionarios (NumDiasPagados, entero o con hasta 3 decimales, por ejemplo 15.5) y con int el SDK no podía enviarlos. El código que llama setDaysPaid(15) o guarda getDaysPaid() en un int deja de compilar: use setDaysPaid(new BigDecimal("15")) o BigDecimal.valueOf(15.5) (no new BigDecimal(15.5) con un double que no sea exacto, que arrastra decenas de decimales y el API rechaza con 400). El SDK lo envía tal cual con su serializador de importes, sin redondear (15.5 viaja como "15.5", que el API timbra como 15.500); con más de 3 decimales el API responde 400 de validación antes del PAC (ver la nota de decimales en Notas del API). Sin asignar (null) ya no se envía (antes viajaba 0); el API lo toma como 0, igual que antes.
  • Se eliminan Person.getCapitalRegime() y Person.setCapitalRegime(String) (estaban @Deprecated): el API no tiene régimen de capital, ignoraba el valor y nunca lo devolvía. El código que los llama deja de compilar: quite esas llamadas y envíe la razón social sin régimen de capital con setLegalName.
  • El API deja de devolver stripeCustomerId y subscriptionStatus en las personas (/api/v4/people y la persona de las reglas de descarga): eran datos internos de Stripe. Este SDK nunca los modeló en Person (estaban comentados y se quitan), así que el código que usa el modelo no cambia; si su integración los lee de la respuesta JSON cruda, quite esa lectura: el campo ya no llega.
  • Se elimina la clase com.fiscalapi.common.StripePaymentMethodDto (stripeId, stripeCustomerId, brand, last4, expMonth, expYear y demás datos de tarjeta): modelaba los métodos de pago de Stripe, que el API retiró en agosto de 2026, y ningún servicio del SDK la usaba. El código que la importa o la instancia deja de compilar: quite el import com.fiscalapi.common.StripePaymentMethodDto; y esos usos; el API no expone métodos de pago ni datos de Stripe que la reemplacen.
  • Se elimina update del servicio de certificados (getTaxFileService().update()): el API retiró PUT /api/v4/tax-files/{id}, que ahora responde 405 (Method Not Allowed); antes nunca actualizaba un certificado (respondía un error). Para cambiar un certificado, suba el nuevo con getTaxFileService().create() y elimine el anterior con getTaxFileService().delete(). El método ya no existe ni en la interfaz ni en la clase del servicio: ITaxFileService extiende la nueva IImmutableFiscalApiService<TaxFile> (consulta, creación y baja, sin update()) y TaxFileService hereda de la nueva BaseImmutableFiscalApiService<TaxFile>, así que client.getTaxFileService().update(...) deja de compilar (cannot find symbol) y el servicio deja de ser un IFiscalApiService<TaxFile>. Los demás servicios no cambian: IFiscalApiService<T> extiende IImmutableFiscalApiService<T> con update() y BaseFiscalApiService<T> hereda de BaseImmutableFiscalApiService<T>.
  • Certificados: el API deja de devolver la contraseña de la llave privada (.key) en las consultas de certificados: getTaxFileService().getList(), getTaxFileService().getById() y la respuesta de getTaxFileService().create() traen password vacía (""); antes, la contraseña en claro (también GET /api/v4/tax-files/all y GET /api/v4/tax-files/load, que este SDK no usa). Solo getTaxFileService().getDefaultValues() la devuelve (a la persona misma o al owner del tenant); getTaxFileService().getDefaultReferences() ya la devolvía vacía. El modelo TaxFile no cambia (su Javadoc lo documenta): la contraseña solo se envía al subir con getTaxFileService().create(). Si su integración lee TaxFile.getPassword() de una de esas consultas, use getTaxFileService().getDefaultValues() o la contraseña que ya conoce. Ningún método ni ejemplo del SDK lee la contraseña de una consulta.

Modelo Person

  • Nuevos miembros:
    • phoneNumber: lo aceptan la creación y la actualización de personas.
    • country: país de residencia fiscal expandido (solo lectura).
    • balances: saldos por tipo de crédito (CreditBalance: creditType y available, solo lectura). Solo aparecen los tipos que la persona ha tenido; un tipo ausente tiene saldo 0, y creditType es Integer para conservar un tipo que el SDK no conoce.
    • isOwner: true si la persona es el owner de su tenant (solo lectura).
    • validTo (LocalDateTime, getValidTo()): fin de vigencia de la persona (solo lectura: se puebla al deserializar y no se envía). La asigna el API; casi siempre es null y es informativa (no limita el timbrado ni el acceso al API).
  • password: requerida al crear; al actualizar, null o vacía conserva la contraseña actual. El API nunca la devuelve.
  • userTypeId: "C" (cliente, el valor por omisión al crear) o "U" (usuario). "T" (tenant) solo llega en respuestas: el API lo rechaza al crear y al actualizar solo lo acepta si la persona ya es "T".
  • Se quitan los campos comentados twoFactorEnabled, stripePaymentMethodId, stripePaymentMethod, stripeCustomerId y subscriptionStatus, que el API ya no devuelve (el comentado validTo pasa a ser miembro real).

Modelos de factura y nómina

  • EmployerData.curp (getCurp()/setCurp(String)): nuevo miembro con la CURP del empleador persona física (Nomina/Emisor/@Curp), que se envía como issuer.employerData.curp en las facturas de nómina por valores (Invoice.getIssuer().getEmployerData()). Antes el SDK no podía enviarla. En las facturas de nómina por referencias el API toma la CURP de la persona emisora, y los datos de empleador de una persona (getPersonService().getEmployerService()) no la guardan (el API la ignora ahí).

Errores de validación (400)

  • ApiResponse.getDetails() en un 400 de validación une todas las fallas como Campo: mensaje separadas por ; (por ejemplo Items[0].Discount: …; Complement.Payroll.DaysPaid: …), igual que los SDK de .NET y Python; antes traía solo la primera falla y el resto se perdía. Con una sola falla el texto no cambia. Si su integración compara el texto completo de getDetails(), compare el campo de cada falla en su lugar.

Modelo DownloadRequest

  • DownloadRequest.getNextAttemptDate() y setNextAttemptDate() quedan obsoletos (@Deprecated) y se eliminarán en una versión mayor. El API ya no devuelve nextAttemptDate en las solicitudes de descarga (getDownloadRequestService().getList(), getDownloadRequestService().getById(), getDownloadRequestService().search() y getDownloadRequestService().create()): siempre era null, porque Fiscalapi no programa un siguiente intento por solicitud (revisa las solicitudes pendientes periódicamente). Los métodos siguen existiendo y getNextAttemptDate() devuelve null, como antes; el código que los usa compila con una advertencia de obsolescencia: quite esa lectura (la fecha del último intento está en getLastAttemptDate()).

Notas del API (sin cambio de forma en el SDK)

  • Person.taxPassword es la contraseña de la llave privada (.key) que la persona guarda en su perfil; el API no la usa para sellar (al timbrar usa la contraseña de los certificados registrados o la de taxCredentials). Solo la reciben con valor la propia persona y el owner del tenant; los demás reciben null. Al actualizar, null la conserva (el SDK no envía los null) y "" la borra.
  • ValidationFailure.attemptedValue (y el placeholder PropertyValue de formattedMessagePlaceholderValues): en un 400 de validación, el valor de un secreto (contraseñas, códigos, tokens, archivos y contraseñas de CSD/FIEL) llega enmascarado como "[masked: n]" (n es su longitud), o "[masked]" si la falla es de un objeto o una lista que lo contiene.
  • TaxCredential.password solo se exige en la llave privada (.key); en el certificado (.cer) el API no la usa.
  • TaxFile.tin es opcional en getTaxFileService().create(): si es null o vacío, el API usa el RFC de la persona. Si se envía, debe ser el RFC de la persona (sin distinguir mayúsculas); si no, el API responde 400 con la falla en Tin (después de comprobar que puede gestionar los certificados de la persona; si no, 403). El API no guarda el valor enviado: el tin del archivo siempre es el RFC de la persona.
  • Las personas ya no traen stripeCustomerId ni subscriptionStatus (datos internos de Stripe). validTo y committedBalance son de solo lectura: el API los ignora al crear o actualizar. committedBalance es un campo heredado que el API ya no calcula y siempre vale 0; para el saldo use availableBalance, availableValidationBalance o balances.
  • Personas: getPersonService().create() y getPersonService().update() con un correo que ya pertenece a otra persona, de esta o de otra cuenta (se compara sin distinguir mayúsculas ni los espacios de los extremos), responden 400 de validación con la falla en Email y el mensaje The email is already registered. (en el SDK, getSucceeded() es false y getDetails() vale "Email: The email is already registered."). Antes respondía otro 400 (una falla de Identity o el genérico A record with the same unique values already exists.). Al actualizar, el correo actual de la persona no cuenta como repetido. El API guarda el correo sin los espacios de los extremos.
  • Personas: getPersonService().update() y getPersonService().delete() pueden responder 409 con details The person was modified by another request (for example, a failed sign-in). Reload it and try again. (en el SDK, getHttpStatusCode() 409 y ese texto en getDetails()) si otra petición modificó a la persona mientras se procesaba: cuenta cualquier escritura a la persona, por ejemplo un inicio de sesión con su correo (exitoso o fallido) o un cambio en su suscripción. No se guarda nada: vuelva a consultar la persona y repita la llamada. Antes la escritura se guardaba igual y podía deshacer el cambio de la otra petición (el bloqueo de la cuenta por intentos fallidos o la sesión recién iniciada). PATCH /api/v4/people/{id}, que este SDK no usa, responde el mismo 409.
  • Personas: getPersonService().delete() revoca también las API keys de la persona, en la misma operación: ya no autenticaban (el API responde 401 a una API key de una persona eliminada) y ahora tampoco aparecen en getApiKeyService().getList() ni en getApiKeyService().getById() (responde 404 con No such resource for the given Id: <id>); antes seguían en el listado como habilitadas (apiKeyStatus 1) y sin persona.
  • Personas: getPersonService().create() y getPersonService().update() validan el correo y la contraseña con las reglas de Identity antes de guardar. Un correo con caracteres fuera de a-z, A-Z, 0-9 y -._@+ (por ejemplo ñ, una vocal acentuada, un apóstrofo o un espacio intermedio) responde 400 de validación con la falla en Email, errorCode InvalidUserName y el mensaje The email contains characters that are not allowed: 'ñ'. Allowed characters: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+ (con los caracteres rechazados entre comillas). Una contraseña que no cumple las reglas (al menos 6 caracteres, con una mayúscula A-Z, una minúscula a-z, un dígito 0-9 y un carácter que no sea letra ni dígito; las letras con acento y la ñ no cuentan como mayúsculas ni minúsculas) responde una sola falla en Password con el mensaje Password must be at least 6 characters long and contain at least one uppercase letter (A-Z), one lowercase letter (a-z), one digit (0-9) and one character that is not a letter or a digit. Letters with accents and ñ are not uppercase or lowercase letters for this rule., el código del primer requisito que no se cumple en errorCode (p. ej. PasswordTooShort o PasswordRequiresUpper) y attemptedValue null (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo). Antes, al crear, esos valores llegaban a Identity y la falla traía el código en propertyName (InvalidUserName, PasswordRequiresUpper…) con el texto de Identity (p. ej. Username '…' is invalid, can only contain letters or digits.) (en el SDK, getDetails() empezaba con InvalidUserName: y ahora con Email:); una contraseña corta como abc daba dos fallas (MinimumLengthValidator y PredicateValidator), y al actualizar el API guardaba un correo con ñ o una contraseña como Ñandú12! (200). Si aun así getPersonService().create() falla en otra validación de Identity, la falla trae en propertyName el campo del request (Email o Password) y el código de Identity en errorCode.
  • Facturas (getInvoiceService().create()): con una moneda extranjera (currencyCode distinto de MXN y de XXX), exchangeRate 0 o negativo responde 400 de validación con la falla en ExchangeRate y el mensaje Con una moneda distinta de MXN, 'exchangeRate' (tipo de cambio) debe ser el del día de la operación (mayor que 0). (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo); antes pasaba la validación del API. Con MXN sigue siendo exactamente 1. El complemento de pagos sigue la misma regla en el tipo de cambio del pago (complement.payment.exchangeRate frente a complement.payment.currencyCode): con MXN, exactamente 1 (si es null, el API usa 1); con otra moneda, mayor que 0. Si no, responde 400 de validación con la falla en Complement.Payment.ExchangeRate (mensaje Con la moneda MXN, 'exchangeRate' (tipo de cambio) debe ser 1. o el de moneda extranjera); antes llegaba al PAC, que lo rechazaba, o el pago salía con totales en 0.
  • Cambios del API en los endpoints de cuenta, que este SDK no usa (se autentica con API key), así que no le afectan: POST /api/v4/account/register y POST /api/v4/account/info con un correo que ya pertenece a otra persona responden 400 de validación con la falla en Email (NewEmail en info) y el mensaje The email is already registered.; POST /api/v4/account/change-password con la contraseña actual incorrecta responde 400 de validación con la falla en CurrentPassword (errorCode PasswordMismatch, mensaje Incorrect password.); en general, las fallas que vienen de Identity en account/change-password, account/info, account/reset-password y account/confirm-email traen en propertyName el campo del request que las causó (CurrentPassword, NewPassword, NewEmail, ResetCode o Code) y el código de Identity en errorCode (PasswordMismatch, InvalidToken, …), en lugar del código como propertyName; POST /api/v4/account/refresh con un refresh token vencido responde 401 Invalid refresh token en lugar de emitir otro (con uno vigente devuelve el mismo token), así que una sesión iniciada con account/login solo se puede renovar hasta 7 días después del login.
  • Facturas (getInvoiceService().create()): con currencyCode XXX (facturas de pago y de traslado), un exchangeRate distinto de 1 y de 0 responde 400 de validación con la falla en ExchangeRate y el mensaje Con la moneda XXX, 'exchangeRate' (tipo de cambio) no aplica: se omite (o se envía 1 o 0). (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo); antes, con un valor mayor que 1 lo rechazaba el PAC (CFDI40116 en un ingreso, CRP20109 en un pago) y con uno entre 0 y 1 o negativo se timbraba ignorándolo. Con 1 (si es null, el API usa 1) o 0 se sigue timbrando, sin TipoCambio en el XML.
  • Facturas de pago (getInvoiceService().create() con complement.payment): la moneda del pago (complement.payment.currencyCode) y la de cada factura pagada (currencyCode de cada PaidInvoice) se validan contra el catálogo c_Moneda del SAT, escritas tal cual: si se omiten, no están en el catálogo (también en minúsculas, como mxn) o son XXX, responde 400 de validación con la falla en Complement.Payment.CurrencyCode o en Complement.Payment.PaidInvoices[n].CurrencyCode (n = posición, desde 0) con el mensaje La moneda '<valor>' no existe en el catálogo c_Moneda del SAT., La moneda de un pago o de un documento pagado no puede ser XXX. o el de campo vacío (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo); antes llegaban al PAC, que las rechazaba (CFDI40113, CRP20213 o CRP20236). La moneda de la factura de pago (currencyCode) debe ser XXX: con otra, 400 de validación en CurrencyCode con La moneda de un comprobante de pago debe ser XXX. (antes, el PAC respondía CRP20104). El valor por omisión de Invoice.currencyCode es MXN: en una factura de pago asígnele XXX. Una factura sin currencyCode responde una sola falla en CurrencyCode (antes, dos).
  • Factura global (getInvoiceService().create() con globalInformation.year): el año acepta el de la fecha de la factura (date) y los 5 anteriores, como el SAT (con fecha en 2026, de 2021 a 2026); antes solo el año en curso o el anterior, y por ejemplo 2021 a 2024 respondían 400 aunque el SAT los timbra. Fuera de ese rango responde 400 de validación con la falla en GlobalInformation.Year y el mensaje 'year' debe ser el año de la fecha del comprobante (2026) o uno de los 5 anteriores. (con el año de la fecha) (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo).
  • Más cambios del API en los endpoints de cuenta, que este SDK no usa: POST /api/v4/account/register (Email, Password), POST /api/v4/account/info (NewEmail, NewPassword), POST /api/v4/account/change-password y POST /api/v4/account/reset-password (NewPassword) aplican las mismas reglas de correo y contraseña que las personas (400 con InvalidUserName o con la falla única de la contraseña); antes register creaba la cuenta con un correo con ñ o una contraseña como Ñandú12!. reset-password valida la contraseña nueva antes del código (con las dos inválidas, la falla es la de NewPassword). En account/info, CurrentPassword ya no se valida con las reglas de formato (una incorrecta responde PasswordMismatch en CurrentPassword) y es obligatoria cuando viene NewPassword: sin ella, 400 de validación en CurrentPassword con Current password is required to set a new password. (antes, el mismo texto en un 400 sin campo). Restablecimiento de contraseña: el código que POST /api/v4/account/forgot-password envía por correo (el resetCode de POST /api/v4/account/reset-password) pasa a ser de 6 dígitos (antes, un texto de unos 350 caracteres), vence a los 15 minutos (antes, a 1 día aunque el correo decía 15 minutos), sirve una sola vez y admite 5 intentos fallidos por persona cada 15 minutos (pedir otro código no da intentos nuevos; agotados, ningún código sirve hasta que terminan esos 15 minutos). Un resetCode que no es de 6 dígitos responde 400 de validación en ResetCode con El código debe tener 6 dígitos. sin gastar intento (también un código largo pedido antes de este cambio: hay que pedir otro); uno equivocado, vencido, ya usado o con los intentos agotados, igual que un correo desconocido, responde 400 en ResetCode (errorCode InvalidToken) con El código no es válido o ya venció. (antes Invalid token.; el mismo texto en Code de account/confirm-email, cuyo enlace no cambia). forgot-password responde Si el correo está registrado y confirmado, te enviamos un código de 6 dígitos para restablecer tu contraseña; vence en 15 minutos. (antes Password reset instructions have been sent if the email is registered and confirmed.) y reset-password, La contraseña se restableció correctamente. (antes Password has been reset successfully.).
  • Certificados: getTaxFileService().delete() hace una baja lógica: el certificado deja de aparecer en todas las consultas y el API deja de usarlo para timbrar; borra el archivo y su contraseña y conserva solo el registro (tipo, RFC, vigencia, quién lo eliminó y cuándo). La respuesta no cambia: 200 con true, o 400 con false si no existe, ya se había eliminado o es de otra persona. Para volver a usar un certificado eliminado, súbalo de nuevo con getTaxFileService().create().
  • Facturas (getInvoiceService().create()): con el RFC genérico (XAXX010101000 o XEXX010101000) el régimen fiscal del receptor debe ser 616 y su código postal el de expeditionZipCode; con el régimen 616 el uso CFDI debe ser S01 o CP01; y una factura de ingreso (I) a XAXX010101000 PUBLICO EN GENERAL debe traer globalInformation. Si no, responde 400 de validación (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo), con todas las fallas juntas, antes de timbrar y sin consumir timbre, en Recipient.TaxRegimeCode (Con el RFC genérico (XAXX010101000 o XEXX010101000) el régimen fiscal del receptor (taxRegimeCode) debe ser 616 (CFDI40159).), Recipient.ZipCode (Con el RFC genérico (XAXX010101000 o XEXX010101000) el código postal del receptor (zipCode) debe ser el lugar de expedición (expeditionZipCode) (CFDI40149).), Recipient.CfdiUseCode (Con el régimen fiscal 616 del receptor, el uso de CFDI (cfdiUseCode) debe ser S01 o CP01 (c_UsoCFDI, CFDI40161).) o GlobalInformation (Un ingreso a XAXX010101000 'PUBLICO EN GENERAL' es una factura global: globalInformation es obligatorio (CFDI40130).). Vale igual con el receptor por valores y por referencia (recipient.id: se revisan los datos de la persona guardada). Antes las rechazaba el PAC con un 400 sin campo (CFDI40159, CFDI40149, CFDI40161 y CFDI40130).
  • Facturas: la factura que devuelven getInvoiceService().create() y getInvoiceService().getById() trae Invoice.getGlobalInformation(): completa (getPeriodicityCode(), getMonthCode() y getYear()) en una factura global, o null si no es global o si es una global timbrada antes de este cambio (el XML timbrado conserva la información global). Antes la respuesta no traía el campo (el modelo quedaba en null).
  • Facturas (getInvoiceService().create()): el tipo de factura y sus complementos deben corresponder, como exige el SAT: I admite complement.localTaxes, complement.cartaPorte y complement.comercioExterior (o ninguno); E, complement.localTaxes y complement.comercioExterior (o ninguno); T requiere complement.cartaPorte o complement.comercioExterior; P requiere complement.payment y N, complement.payroll; T, P y N no admiten otro complemento. Si falta el complemento que el tipo requiere o viene uno que no le corresponde (por ejemplo payroll en un ingreso, cartaPorte en un pago o localTaxes en un traslado), responde 400 de validación con la falla en Complement (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo), antes de timbrar y con el mensaje del tipo: Un comprobante de tipo I (ingreso) no lleva complement o solo lleva complement.localTaxes, complement.cartaPorte o complement.comercioExterior., Un comprobante de tipo E (egreso) no lleva complement o solo lleva complement.localTaxes o complement.comercioExterior., Un comprobante de tipo T (traslado) requiere complement.cartaPorte o complement.comercioExterior y no admite otro complemento., Un comprobante de tipo P (pago) requiere complement.payment y no admite otro complemento. o Un comprobante de tipo N (nómina) requiere complement.payroll y no admite otro complemento. En I y E, un complement vacío (sin complementos, o con todos en null) equivale a no enviarlo; en T, P y N, responde el 400 de este tipo. Antes, sin el complemento que el tipo requiere o con un complement vacío, respondía un error 500 (No mapper found for invoice request); con un complemento que no le correspondía, la factura llegaba al PAC, que la rechazaba (por ejemplo CRP20101 con payment fuera de P o CP114 con cartaPorte en P o E), o el complemento sobrante se omitía sin aviso (por ejemplo localTaxes en un pago), y un I con payroll se enviaba como nómina (N). POST /api/v4/invoices/calculate, que este SDK no usa, responde igual, y con un typeCode fuera del catálogo responde 400 en TypeCode (antes, 500).
  • Facturas (getInvoiceService().create()): el 400 de un typeCode fuera del catálogo dice El tipo de comprobante '<valor>' no existe en el catálogo c_TipoDeComprobante del SAT. (antes Please use the SAT catalog values: I,E,T,P,N,P, con la P repetida; ver la nota de claves de catálogo del timbrado).
  • Facturas (getInvoiceService().create()): una fecha ausente, null, vacía o que no se reconoce como fecha responde 400 de validación en el campo (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo) en lugar de llegar al PAC como 0001-01-01 (que la rechazaba con CFDI40101 o CFDI40999): date → Date con 'date' es obligatorio y debe ser una fecha con el formato yyyy-MM-ddTHH:mm:ss.; complement.payment.paymentDate → Complement.Payment.PaymentDate con 'paymentDate' es obligatorio y debe ser una fecha con el formato yyyy-MM-ddTHH:mm:ss.; y complement.payroll.paymentDate, initialPaymentDate y finalPaymentDate → Complement.Payroll.PaymentDate, Complement.Payroll.InitialPaymentDate y Complement.Payroll.FinalPaymentDate con '<campo>' es obligatorio y debe ser una fecha con el formato yyyy-MM-dd. ('paymentDate', 'initialPaymentDate' o 'finalPaymentDate'). Una fecha del año 1 (0001-01-01…) cuenta como ausente y responde el mismo 400 en el campo; antes, con la zona horaria de México (America/Mexico_City), el API la convertía a otra hora del año 1, no la reconocía como ausente y la factura llegaba al PAC (CFDI40101 o CFDI40999).
  • Facturas (getInvoiceService().create()): una factura I, E o T sin items (o con la lista vacía) responde 400 de validación con la falla en Items y el mensaje items debe tener al menos un concepto en los comprobantes de tipo I, E y T. (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo), sin ir al PAC; antes la rechazaba el PAC (CFDI40999 o CP999: el nodo Conceptos es obligatorio en el CFDI). En P y N se siguen omitiendo: el API genera el concepto. POST /api/v4/invoices/calculate, que este SDK no usa, aplica la misma regla (antes respondía 400 sin mensaje) y, con un total 0 (todo T y P, o un ingreso con descuento del 100 %), responde 200 con el cálculo (antes, 400 con succeeded false).
  • Facturas con Carta Porte o Comercio Exterior (getInvoiceService().create()): el API valida los datos de esos complementos antes de timbrar (obligatorios, longitudes, patrones y catálogos del SAT): una falla responde 400 de validación con la falla en el campo, en Complement.CartaPorte.… o Complement.ComercioExterior.… (por ejemplo Complement.CartaPorte.Ubicaciones[0].NumEstacionId con NumEstacionId must be a valid c_Estaciones value.) (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo), sin consumir timbre; antes esas reglas no se aplicaban y el PAC rechazaba la factura (CPxxx, CCExxx o CFDI40101). La fecha de salida o llegada de cada ubicación de Carta Porte sigue la regla de las demás fechas: ausente, vacía, del año 1 o ilegible responde 400 en Complement.CartaPorte.Ubicaciones[n].FechaHoraSalidaLlegada con 'fechaHoraSalidaLlegada' es obligatorio y debe ser una fecha con el formato yyyy-MM-ddTHH:mm:ss. (antes llegaba al PAC como 0001-01-01). Las claves de estación (c_Estaciones) se escriben como en el catálogo del SAT, con acento cuando lo llevan: Juárez0 se acepta y Juarez0 responde 400. El NumRegIdTrib del receptor del complemento Comercio Exterior (ComercioExteriorReceptor.setNumRegIdTrib()) se ignora, como antes: el API usa el del receptor del CFDI (InvoiceRecipient.setForeignTin(), o el de la persona receptora por referencias); puede omitirlo.
  • Facturas: los errores de lectura del request (un valor de otro tipo, JSON mal formado, cuerpo vacío o un campo obligatorio ausente o null dentro de un complemento, por ejemplo Complement.CartaPorte.TranspInternacId con 'transpInternacId' no debe estar vacío.) responden el mismo 400 de validación que las demás reglas, con la falla en el campo, en lugar de un ProblemDetails (application/problem+json): en el SDK, antes getMessage() llegaba vacío y getDetails() en null (el SDK no lee ProblemDetails); ahora getSucceeded() es false, getMessage() es Sorry, it's not me, it's you. y getDetails() trae la falla como Campo: mensaje. En esas fallas errorCode es null. Un cuerpo que no se puede leer responde una sola falla (así getDetails() muestra solo la falla real): un valor de otro tipo, en su ruta (por ejemplo $.items[0].quantity), y un JSON mal formado, en $ (la raíz del JSON), ambos con el texto en inglés del lector de JSON; un cuerpo vacío o null, en $ con El cuerpo de la petición es requerido y no debe estar vacío. El ProblemDetails de antes traía además en errors la clave del parámetro del endpoint (command, The command field is required.) y, con un cuerpo vacío, la clave vacía (""); ya no llegan. Vale para todos los endpoints.
  • Mensajes de validación en español (todos los endpoints): los errorMessage generales de las validaciones del API pasan de inglés a español, por ejemplo '<Campo>' no debe estar vacío. (antes '<Campo>' must not be empty.) o 'tipoCambioUSD' debe ser mayor que '0'., y también los de lectura del request: 'transpInternacId' no debe estar vacío. (antes The TranspInternacId field is required.), El cuerpo de la petición es requerido y no debe estar vacío. (antes A non-empty request body is required.) y El valor 'abc' no es válido para PageNumber. (antes The value 'abc' is not valid for PageNumber.). En el SDK cambia el texto de getDetails() (cada falla); propertyName, errorCode, message (Sorry, it's not me, it's you.), details (One or more validation failures have occurred.) y la forma del 400 no cambian. Conservan su texto los mensajes propios de cada regla (varios en inglés, como los de personas y cuentas citados en estas notas; los del timbrado pasan al español, ver la nota de mensajes del timbrado) y los del lector de JSON (JSON mal formado o un valor de otro tipo, en inglés). Si su integración compara el texto del mensaje, use el campo de la falla (getPropertyName()) en su lugar.
  • Certificados, timbrado y cancelación por referencias: getTaxFileService().getDefaultReferences(), getTaxFileService().getDefaultValues(), getInvoiceService().create() con el emisor por referencia (issuer.id) y getInvoiceService().cancel() de una factura emitida por referencias, cuando la persona (o el emisor) solo tiene vigente uno de los dos archivos del CSD, responden 404 con No se encontró la llave privada de la persona: <id> o No se encontró el certificado de la persona: <id> (en el SDK, getHttpStatusCode() 404 y ese texto en getDetails()); suba el archivo que falta. Antes, las referencias por defecto y la cancelación respondían un error 500, getDefaultValues() respondía 200 con un solo archivo y el timbrado respondía 400 con Ambos certificado y clave privada son requeridos para crear un credencial FIEL o CSD. Sin ningún archivo vigente, las referencias por defecto, el timbrado y la cancelación responden el mismo 404 con No se encontró tal recurso. Verifica los archivos fiscales de la persona: <id>; antes, ese 404 solo lo daban las referencias por defecto, el timbrado respondía 400 con No se proporcionaron archivos fiscales para crear un credencial FIEL o CSD. y la cancelación 400 en inglés con When issuer data is missing, issuer's certificates must be configured. getDefaultValues() sin ningún archivo vigente sigue respondiendo 200 con la lista vacía. En el timbrado, <id> es el issuer.id enviado; en la cancelación, el id del emisor de la factura.
  • Certificados: getTaxFileService().getDefaultReferences() aplica la misma regla que getTaxFileService().getDefaultValues(): solo la persona misma o el owner del tenant obtienen las referencias de sus certificados. Si la API key es de una persona que no es el owner y pide las referencias de otra persona, responde 403 con Solo el propietario de la cuenta puede gestionar las credenciales de otra persona: correo, contraseña, API keys y certificados. (en el SDK, getHttpStatusCode() 403 y ese texto en getDetails()); antes respondía 200 con los ids y la vigencia de los certificados de esa persona.
  • Reglas de reporte (/api/v4/reportrules, que este SDK no usa): PUT y DELETE sobre la regla predeterminada de FiscalAPI (compartida con todas las cuentas) responden 403 con Operation denied. (en el SDK, getHttpStatusCode() 403 y ese texto en getDetails()); antes el API permitía modificarla o borrarla. DELETE de un id que no existe responde 404 (antes, 400 con false).
  • Facturas por referencias (getInvoiceService().create() con issuer.id, recipient.id o items[n].id): cuando a la persona o al producto guardado le falta un dato fiscal, el API responde 400 en español con un solo formato, La persona emisora '<id>' no tiene <dato> (<campo>)., La persona receptora '<id>' no tiene <dato> (<campo>). o El producto '<id>' no tiene <dato> (<campo>)., por ejemplo La persona emisora '<id>' no tiene RFC (tin). o La persona receptora '<id>' no tiene uso de CFDI (satCfdiUseId). (en el SDK, getHttpStatusCode() 400 y ese texto en getDetails()). En las personas el dato es RFC (tin), razón social (legalName), régimen fiscal (satTaxRegimeId) o código postal (zipCode), y en la receptora también uso de CFDI (satCfdiUseId); en el producto, clave de producto o servicio (satProductCodeId), clave de unidad (satUnitMeasurementId), objeto de impuesto (satTaxObjectId) o descripción (description). Antes, 400 en inglés sin el id: Issuer Tin is required, Issuer LegalName is required, Issuer SatTaxRegimeId is required, Issuer ZipCode is required, Recipient <Campo> is required y Product <Campo> is required. El código 400 y la forma del error no cambian; un id que no existe sigue respondiendo 404 con No such resource for the given Id: <id>.
  • Cancelación por referencias (getInvoiceService().cancel() con id): con el id de una factura que no existe o que es de otra cuenta, el API responde 404 con No such resource for the given Id: <id> (en el SDK, getHttpStatusCode() 404 y ese texto en getDetails()); antes respondía un error 500 (Something went wrong on our end.). Si la persona emisora de la factura ya no existe (se eliminó), responde 400 con No se encontró la persona emisora '<id>' de esta factura. Para cancelarla, envía invoiceUuid, tin y taxCredentials (cancelación por valores). (<id> es el id del emisor; antes, 400 en inglés con When issuer data is missing, issuer's Id must be provided.): cancélela por valores con invoiceUuid, tin y taxCredentials. Si la persona emisora no tiene RFC, responde 400 con La persona emisora '<id>' no tiene RFC (tin). (antes La persona '<id>' no tiene datos fiscales).
  • Reglas de descarga (getDownloadRuleService().create(), getDownloadRuleService().update() y getDownloadRuleService().delete()): solo el owner del tenant crea, actualiza o elimina una regla de la persona owner (personId del owner). Con la API key de otra persona del tenant, el API responde 403 con Solo el propietario de la cuenta puede modificar al propietario de la cuenta. (en el SDK, getHttpStatusCode() 403 y ese texto en getDetails()) y la regla no se crea, no cambia ni se elimina; antes el API lo permitía (al crear, respondía 200 o el error de asientos o de FIEL; ahora el 403 llega antes de esas revisiones). Las reglas de las demás personas no cambian.
  • Cambio de valor (corrige un error), Descarga masiva: getDownloadRequestService().getXmls() (GET /api/v4/download-requests/{id}/xmls, Xml.getDate()) y getDownloadRequestService().getMetadataItems() (GET /api/v4/download-requests/{id}/meta-items, MetadataItem.getInvoiceDate(), MetadataItem.getSatCertificationDate() y MetadataItem.getCancellationDate()) devuelven ahora la fecha y hora del CFDI/SAT tal cual (la hora local del lugar de expedición, igual a la Fecha del XML, y la de la metadata del SAT), sin depender de X-TIME-ZONE (FiscalApiSettings.setTimeZone()); antes se les restaba el desfase de esa zona (6 h con America/Mexico_City, la predeterminada del SDK). Igual en GET /api/v4/download-xmls/{id} y GET /api/v4/download-meta-items/{id}, que este SDK no usa. Ejemplo: XML con Fecha="2025-06-18T18:58:30": antes "date": "2025-06-18T12:58:30.000" (Xml.getDate() a las 12:58:30), ahora "date": "2025-06-18T18:58:30.000" (Xml.getDate() a las 18:58:30). Ningún tipo cambia; si su código compensaba el desfase (por ejemplo, sumando 6 h), quite esa compensación. Los demás campos de fecha (getCreatedAt(), getUpdatedAt(), getStartDate(), getEndDate() y getLastAttemptDate()) no cambian.
  • Consultas por id: con un id que no existe o que es de otra cuenta, getInvoiceService().getById(), getInvoiceService().getXml(), getInvoiceService().getPdf() (invoiceId), getInvoiceService().status() (id), getPersonService().getById(), getPersonService().update(), getPersonService().delete(), getStampService().getById(), getProductService().getById(), getDownloadRuleService().getById(), getDownloadRuleService().delete(), getDownloadRequestService().getById(), getDownloadRequestService().delete(), getDownloadRequestService().downloadPackage(), getDownloadRequestService().downloadSatRequest() y getDownloadRequestService().downloadSatResponse() responden 404 con No such resource for the given Id: <id> (en el SDK, getHttpStatusCode() 404 y ese texto en getDetails()), la misma respuesta que ya daban otras rutas por id (por ejemplo getInvoiceService().send()); el message es Sorry, it's not me, it's you. Antes cada una tenía su propio texto: getInvoiceService().getById(), Not found such resource: <id>; getInvoiceService().getXml(), No such resource for the given id: <id>; getInvoiceService().status(), getStampService().getById(), getDownloadRuleService().delete(), getDownloadRequestService().delete(), getDownloadRequestService().downloadSatRequest() y getDownloadRequestService().downloadSatResponse(), No such resource for given id: <id>; getPersonService().getById() y getPersonService().delete(), No such resource for the given id:<id>; getProductService().getById(), Product with id: <id> not found; getDownloadRuleService().getById(), Rule with id: <id> not found; getDownloadRequestService().getById(), Sat request with id: <id> not found; y getDownloadRequestService().downloadPackage(), No existe una solicitud de descarga con id <id>. con el message Not Found. getInvoiceService().getPdf() y getPersonService().update() respondían 400 (con No such resource for given id: <id> y No such resource for the given id: <id>); ahora, 404. PATCH /api/v4/people/{id}, que este SDK no usa, responde igual que getPersonService().update(). Si su integración reconoce el "no encontrado" por el texto, use el código 404.
  • Catálogos (getCatalogService().getRecordById()): una clave que no existe en el catálogo responde 404 con No such resource for the given Id: <id> (en el SDK, getHttpStatusCode() 404 y ese texto en getDetails()); antes respondía 400 con Record not found for <catálogo> with ID <id> en message y details vacío. Un nombre de catálogo que no existe responde 404 (ver la nota de nombres de catálogo); las claves que existen responden igual que antes.
  • Productos y datos de empleado y empleador (BE-238/BE-239; getProductService().create() y getProductService().update(), getPersonService().getEmployeeService().create() y .update(), getPersonService().getEmployerService().create() y .update()): unitPrice de un producto debe ser mayor que 0 y como máximo 999999999999.999999 (12 enteros y 6 decimales); por encima, 400 de validación en UnitPrice con El precio unitario no puede ser mayor a 999999999999.999999. (antes, 500). Las tasas o cuotas de productTaxes (rate) deben ser mayores o iguales a 0 y como máximo 999999999999.999999 (una cuota fija puede ser mayor que 1): 400 en ProductTaxes[n].Rate con La tasa o cuota debe ser mayor o igual a 0. o La tasa o cuota no puede ser mayor a 999999999999.999999. (antes, una tasa negativa se guardaba y una mayor respondía 500). baseSalaryForContributions e integratedDailySalary del empleado, y ownResourceAmount del empleador, deben ser mayores o iguales a 0 (el último, si se envía) y como máximo 999999999999.999999: 400 en el campo con SalarioBaseCotApor debe ser mayor o igual a 0., SalarioDiarioIntegrado no puede ser mayor a 999999999999.999999. o MontoRecursoPropio no puede ser mayor a 999999999999.999999. (antes, un salario negativo se guardaba y uno mayor, igual que un ownResourceAmount mayor, respondía 500). Un null en ownResourceAmount no se valida.
  • Productos (getProductService().create() y getProductService().update()): las claves de catálogo del producto y de sus impuestos se validan antes de guardar y responden 400 de validación con la falla en el campo y el mensaje en español (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo): SatUnitMeasurementId, La clave de unidad '<valor>' no existe en el catálogo c_ClaveUnidad del SAT.; SatTaxObjectId, El objeto de impuesto '<valor>' no existe en el catálogo c_ObjetoImp del SAT.; SatProductCodeId (si se envía, también vacío), La clave de producto o servicio '<valor>' no existe en el catálogo c_ClaveProdServ del SAT.; y en cada impuesto (n = posición, desde 0), ProductTaxes[n].TaxId, El impuesto '<valor>' no existe en el catálogo c_Impuesto del SAT.; ProductTaxes[n].TaxTypeId, El tipo factor '<valor>' no existe en el catálogo c_TipoFactor del SAT.; y ProductTaxes[n].TaxFlagId, La naturaleza del impuesto '<valor>' no es válida: usa T (traslado) o R (retención). Una clave vacía (salvo SatProductCodeId) responde el mensaje de campo vacío. Antes, un SatProductCodeId o un impuesto inválido respondía 400 sin campo, con Sorry, it's not you, it's me. y The operation cannot be completed because the record is related to another non-existent record., y la clave de unidad y el objeto de impuesto, Please use the SAT catalog values: <todas las claves del catálogo>. SatProductCodeId en null sigue permitido. Un impuesto repetido en la lista de impuestos del producto (mismo taxId, taxTypeId, taxFlagId y rate; la tasa se compara con 6 decimales, así que 0.16 y 0.1600004 son el mismo) responde 400 de validación antes de guardar, con la falla en ProductTaxes[n] (n = la segunda aparición) y el mensaje El impuesto '<taxId>' <taxTypeId> <rate> (<taxFlagId>) se repite en el producto: cada impuesto va una sola vez. (por ejemplo, El impuesto '002' Tasa 0.16 (T) se repite en el producto: cada impuesto va una sola vez.; la tasa sale como se envió, redondeada a 6 decimales); antes llegaba a la base de datos y respondía el 400 sin falla de validación Ya existe un registro con los mismos datos únicos. (ver la nota «Errores del servidor»). Una actualización rechazada ya no borra los impuestos guardados del producto (antes, getProductService().update() con un impuesto inválido respondía el error y dejaba el producto sin impuestos).
  • Personas (getPersonService().create() y getPersonService().update(); también PATCH /api/v4/people/{id}, que este SDK no usa): el régimen fiscal, el uso de CFDI, el país y el tipo de persona que no son del catálogo responden 400 de validación con la falla en el campo y el mensaje en español (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo): SatTaxRegimeId, El régimen fiscal '<valor>' no existe en el catálogo c_RegimenFiscal del SAT.; SatCfdiUseId, El uso de CFDI '<valor>' no existe en el catálogo c_UsoCFDI del SAT.; CountryId, El país '<valor>' no existe en el catálogo c_Pais del SAT.; y UserTypeId, UserTypeId debe ser uno de: C, U. 'T' está reservado y solo se permite cuando la persona ya es Tenant. (al actualizar, T se sigue aceptando si la persona ya es T). Antes, al actualizar, el régimen fiscal y el uso de CFDI respondían Please use the SAT catalog values: <lista del catálogo>; al crear, y el país en las dos operaciones, 400 sin campo con Sorry, it's not you, it's me. y The operation cannot be completed because the record is related to another non-existent record.; y el tipo de persona al crear, UserTypeId must be one of: C, U. Los valores vacíos se siguen aceptando (se guardan sin valor).
  • Facturas (getInvoiceService().create()): una moneda que no está en el catálogo c_Moneda (también en minúsculas, como mxn) responde 400 de validación en CurrencyCode con La moneda '<valor>' no existe en el catálogo c_Moneda del SAT. (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo); antes, Please use the SAT catalog values: <todas las claves de c_Moneda>. La moneda del pago y la de cada factura pagada usan el mismo texto (ver la nota de facturas de pago).
  • Catálogos (getCatalogService().getRecordById(), getCatalogService().searchCatalog() y getDownloadCatalogService().listCatalog()): un nombre de catálogo que no existe responde 404 con No existe el catálogo '<nombre>'. (en el SDK, getHttpStatusCode() 404 y ese texto en getDetails()); el message es Sorry, it's not me, it's you. Antes respondía 400 con Invalid catalog name: <nombre> en message, o un error 500 con algunos nombres (por ejemplo Database o Users). El nombre distingue mayúsculas y minúsculas (satpaymentforms no es SatPaymentForms), como antes; los nombres válidos son los que devuelven GET /api/v4/catalogs y GET /api/v4/download-catalogs, y responden igual que antes. GET /api/v4/catalogs/<nombre> y GET /api/v4/catalogs/<nombre>/load, que este SDK no usa, responden el mismo 404 (antes, 400 y 500).
  • Productos (getProductService().create() y getProductService().update()): una descripción (description) de más de 255 caracteres responde 400 de validación con la falla en Description y el mensaje 'Description' debe ser menor o igual que 255 caracteres. Ingresó <n> caracteres. (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo). Antes, al crear, respondía 400 sin campo con Sorry, it's not you, it's me. y The operation cannot be completed because the record is related to another non-existent record., y al actualizar, 400 con One or more fields exceed the maximum allowed length. Con 255 caracteres o menos, sin cambio.
  • Claves de catálogo del timbrado (getInvoiceService().create()) y de los datos de empleado y empleador (getPersonService().getEmployeeService().create() y .update(); getPersonService().getEmployerService().create() y .update()): lo que se acepta o rechaza no cambia (mismo 400 y misma falla en el campo), pero el mensaje pasa al español, con el valor y el catálogo y sin la lista de claves (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo): <El dato> '<valor>' no existe en el catálogo <c_Catálogo> del SAT. en TypeCode (c_TipoDeComprobante), ExportCode (c_Exportacion), PaymentFormCode (c_FormaPago), PaymentMethodCode (c_MetodoPago), el régimen fiscal del emisor, del receptor y a cuenta de terceros (c_RegimenFiscal), el uso de CFDI del receptor (c_UsoCFDI), la unidad (c_ClaveUnidad) y el objeto de impuesto (c_ObjetoImp) de cada concepto, el impuesto (c_Impuesto) y el tipo factor (c_TipoFactor) de los impuestos del concepto y del documento pagado, la periodicidad (c_Periodicidad) y el mes o bimestre (c_Meses) de la información global y el tipo de relación (c_TipoRelacion); en VersionCode, La versión de CFDI '<valor>' no es válida: usa 4.0.; en la naturaleza (TaxFlagCode) de los impuestos del concepto, del documento pagado y de los impuestos locales, La naturaleza del impuesto '<valor>' no es válida: usa T (traslado) o R (retención).; en el código postal de expedición, del receptor y a cuenta de terceros, El código postal '<valor>' debe tener 5 caracteres (catálogo c_CodigoPostal del SAT). (en el del receptor también … debe tener 5 dígitos (catálogo c_CodigoPostal del SAT). y, si falta, El código postal es obligatorio.); en la clave de producto o servicio del concepto y de sus partes, La clave de producto o servicio '<valor>' debe tener 8 caracteres (catálogo c_ClaveProdServ del SAT).; en la moneda de una factura de pago (CurrencyCode), La moneda de un comprobante de pago debe ser XXX.; en los catálogos de Nómina 1.2 (tipo de nómina, percepción, horas extra, otro pago, deducción e incapacidad, y en los datos de empleado y empleador tipo de contrato, régimen, periodicidad de pago, entidad federativa, riesgo de puesto, banco, tipo de jornada y origen del recurso), La clave '<valor>' no existe en el catálogo <c_Catálogo> del SAT o ya no está vigente.; y en Sindicalizado, El valor '<valor>' de Sindicalizado no es válido: usa Sí o No. Antes, Please use the SAT catalog values: <lista del catálogo> (en nómina, (<catálogo>): <lista>; a cuenta de terceros, Utilice los valores del catálogo SAT: <lista>), The value does not exist in the SAT catalog <c_X> or is no longer valid., The TaxFlagCode must be either…, Please use the SAT 'c_CodigoPostal' catalog values. o The currency of an invoice with the payment complement must be XXX. Cada campo responde una sola falla: en los campos obligatorios con esta regla (por ejemplo VersionCode, TypeCode, los impuestos o las claves de nómina) un valor vacío o ausente responde solo el mensaje de campo vacío (antes, también el del catálogo), y un ExpeditionZipCode inválido, una falla (antes, dos iguales). POST /api/v4/invoices/calculate, que este SDK no usa, responde igual. Si su integración compara el texto del mensaje, use el campo de la falla.
  • Facturas (getInvoiceService().create()): antes de cobrar el timbre y de llamar al PAC, el API arma la factura que guardará después de timbrar y revisa que cada valor quepa en ella. Un texto más largo que su máximo, o un importe o una cantidad con más dígitos enteros que los admitidos (redondeado a sus decimales; por ejemplo, la cantidad del concepto y el monto del pago admiten 12: valores menores que 1000000000000), responde 400 de validación con Sorry, it's not me, it's you. y One or more validation failures have occurred., una falla por valor, sin timbrar ni consumir timbre (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo). El mensaje es Admite como máximo <n> caracteres y se recibieron <m>. en un texto (attemptedValue en null: puede ser un dato personal) o Admite como máximo <d> dígitos enteros (valores menores que <10^d>); se recibió <valor>. en un importe o una cantidad (attemptedValue es el número). El campo de la falla (propertyName) es la ruta del dato en la factura guardada, no la del request: InvoiceItems[0].Quantity (por items[0].quantity), InvoicePayment.Amount (por complement.payment.amount) o Payroll.Deductions[0].Code (por complement.payroll.deductions[0].code); otras rutas son, por ejemplo, InvoicePayment.PaidInvoices[0].Number, Payroll.Earnings.Earnings[0].Concept, InvoiceRecipient.EmployeeData.Curp, InvoiceIssuer.EmployerData.EmployerRegistration, CartaPorte.Mercancias[0].ClaveUnidad y ComercioExterior.Emisor.Domicilio.Colonia. Antes, un texto más largo lo rechazaba el PAC con su código (por ejemplo CFDI40999, CRP20999, CP999, NOM111 o CCE999), y un importe o una cantidad de 13 o más dígitos enteros se timbraba y la factura se cancelaba en segundo plano (400 con Sorry, it's not you, it's me.). Las reglas propias de los campos del request (por ejemplo, el correo del receptor de hasta 256 caracteres, itemSku de hasta 100 o Sindicalizado) no cambian y responden antes, en el campo del request.
  • Facturas (getInvoiceService().create()): InvoiceItem.setUnitOfMeasurement() (items[n].unitOfMeasurement) se acepta y se ignora, como antes: el API no lo lee, el CFDI se timbra sin el atributo Unidad del concepto y la factura guardada lo devuelve en null. Puede omitirlo; la unidad del concepto es setUnitOfMeasurementCode() (catálogo c_ClaveUnidad).
  • Mensajes del timbrado en español (getInvoiceService().create(); también POST /api/v4/invoices/calculate, que este SDK no usa): lo que se acepta o rechaza no cambia (mismo 400 de validación, mismas fallas y mismo campo; propertyName sigue siendo la ruta en PascalCase, por ejemplo Items[0].ItemTaxes[0].TaxCode), pero cambia el texto (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo). Los mensajes propios de las reglas del timbrado que seguían en inglés pasan al español (los que citan las notas anteriores ya están al día): en Issuer.Tin y Recipient.Tin, 'tin' no tiene el formato de RFC del SAT: 3 letras (persona moral) o 4 (persona física), la fecha AAMMDD y 3 caracteres de homoclave. (antes Does not match the pattern defined in the type defined for expressing Mexican Federal Taxpayer Registry Keys (RFC)); en Number, 'number' (folio) debe tener de 1 a 40 caracteres sin el carácter '|'; si se omite (o va null o vacío), se genera automáticamente.; en la tasa de los impuestos del concepto y del documento pagado (TaxRate), 'taxRate' (tasa o cuota) debe ser mayor o igual que 0.; en una retención exenta (TaxTypeCode), Una retención (taxFlagCode R) no puede tener 'taxTypeCode' Exento.; en RelatedInvoices, Todos los 'relatedInvoices' deben tener el mismo relationshipTypeCode.; en los impuestos locales (Complement.LocalTaxes), 'taxes' requiere al menos un impuesto local., 'taxName' (nombre del impuesto local) es obligatorio; p. ej., ISN o CEDULAR. y 'taxRate' debe ser mayor que 0; p. ej., 0.030000 para 3 %., y en su naturaleza (TaxFlagCode) el mismo texto que en los demás impuestos, La naturaleza del impuesto '<valor>' no es válida: usa T (traslado) o R (retención). (antes The TaxFlagCode must be either 'T' (Traslado) or 'R' (Retención).); en los impuestos del documento pagado, 'base' debe ser mayor o igual que 0 (0 toma el subTotal del documento pagado). y, con taxTypeCode Cuota, Un impuesto con taxTypeCode Cuota requiere 'base' (unidades): el subTotal del documento pagado es un importe.; en el documento pagado, 'equivalence' debe ser 1 cuando la moneda del documento pagado es la del pago. y 'subTotal' debe ser mayor que 0 cuando un impuesto del documento pagado tiene base 0 (base 0 toma el subTotal).; en Recipient.ForeignTin, 'foreignTin' (NumRegIdTrib) debe tener de 1 a 40 caracteres.; en Recipient.Email, 'email' no es un correo electrónico válido.; en la descripción del concepto, 'description' debe tener de 1 a 1000 caracteres y no puede contener el carácter '|'.; en las credenciales del emisor por valores (Issuer.TaxCredentials, y en la cancelación por valores), Debes proporcionar exactamente 2 credenciales fiscales (sellos SAT): una de fileType=0 (CertificateCsd) y otra de fileType=1 (PrivateKeyCsd). (el texto que ya daba la cancelación), 'fileType' '<valor>' no es un tipo de archivo válido. y 'password' es obligatorio para la llave privada.; con más de 1500 conceptos, el 400 sin campo trae Una factura admite como máximo 1500 conceptos (items).; en Carta Porte, 'ubicaciones' requiere al menos 2 ubicaciones. y Se permiten como máximo 2 remolques.; y en Comercio Exterior, 'certificadoOrigen' debe ser 0 (no funge) o 1 (funge como certificado de origen)., 'mercancias' requiere al menos una mercancía. y 'domicilios' del destinatario requiere al menos un domicilio. Además, en los mensajes generales de validación del timbrado el campo se nombra como en la API (camelCase, como en el JSON) y no con el nombre de la propiedad separado en palabras: 'taxCode' no debería estar vacío. (antes 'Tax Code' no debería estar vacío.), 'quantity' debe ser mayor que '0'. y, en la lectura del request, 'code' no debe estar vacío. (antes 'Code' no debe estar vacío.); vale también donde otro endpoint reutiliza esos tipos, como las credenciales de la cancelación por valores. Los demás endpoints conservan el nombre separado en palabras. Siguen en inglés los mensajes de catálogo de Carta Porte y Comercio Exterior (<Campo> must be a valid c_<Catálogo> value.), los de los archivos de certificados (por ejemplo The certificate (.cer) belongs to RFC …) y los del lector de JSON. Si su integración compara el texto del mensaje, use el campo de la falla.
  • Facturas (getInvoiceService().create(); también POST /api/v4/invoices/calculate, que este SDK no usa): un total de 2,147,483,648 o más ya no responde 500 con Value was either too large or too small for an Int32. (antes del PAC, sin consumir timbre): si cabe en la factura se timbra (o se calcula) como cualquier otro y, si no, responde el 400 de los valores que no caben en la factura (ver esa nota). Además, el importe con letra que se guarda con la factura y sale en su PDF dice MILLONES de 2 a 9 millones (antes DOS MILLON …) y es correcto con mil millones o más (antes leía solo parte de los millones); el XML timbrado no lleva el importe con letra. Las facturas ya timbradas conservan el texto con el que se guardaron.
  • Cancelación (getInvoiceService().cancel(), por valores y por referencias): cancellationReasonCode debe ser una clave del catálogo c_MotivoCancelacion del SAT (01, 02, 03 o 04); otro valor responde 400 de validación con la falla en CancellationReasonCode y el mensaje El motivo de cancelación '<valor>' no existe en el catálogo c_MotivoCancelacion del SAT. (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo), sin llamar al PAC. Antes solo se exigía que no estuviera vacío y cualquier otro valor llegaba al PAC (el de pruebas llegó a cancelar con un motivo inexistente; el SAT lo rechaza con el código 311). Un motivo vacío sigue respondiendo el mensaje de campo vacío, y 01 sin replacementUuid, la falla en ReplacementUuid, como antes.
  • Certificados (getTaxFileService().create()): fileType 4 (Pfx), que el API aceptaba sin documentarlo y que ningún flujo usa, responde 400 de validación con la falla en FileType y el mensaje 'File Type' 4 (Pfx) no se admite: el certificado (.cer) y la llave privada (.key) se cargan por separado (fileType 0 y 1 para el CSD, 2 y 3 para la FIEL). (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo); antes respondía 200 y guardaba el archivo. Los tipos admitidos son 0 y 1 (certificado y llave privada del CSD) y 2 y 3 (certificado y llave privada de la FIEL); otro valor sigue respondiendo 'File Type' tiene un rango de valores que no incluye '<valor>'.
  • Elementos null en listas (facturas getInvoiceService().create(), también POST /api/v4/invoices/calculate, que este SDK no usa; productos getProductService().create() y getProductService().update(); cancelación getInvoiceService().cancel(), por valores y por referencias): cada elemento de una lista de objetos del cuerpo debe ser un objeto. Un elemento null (por ejemplo, en items, relatedInvoices, las percepciones de nómina, productTaxes o taxCredentials) responde 400 de validación con Sorry, it's not me, it's you. y One or more validation failures have occurred., una falla por cada elemento null (en el SDK, getSucceeded() es false y getDetails() trae las fallas con su campo), antes de las demás validaciones y sin llamar al PAC ni consumir timbre. propertyName es la ruta del elemento con su posición (por ejemplo, RelatedInvoices[0], Items[1], Complement.Payroll.Earnings.Earnings[0], ProductTaxes[1] o TaxCredentials[0]) y el mensaje es '<campo>' no admite elementos null: cada elemento debe ser un objeto., con el campo como en la API en las facturas ('relatedInvoices' no admite elementos null: cada elemento debe ser un objeto.) y separado en palabras en productos y cancelación ('Product Taxes', 'Tax Credentials'). Antes, la mayoría de estos casos respondía 500 (Something went wrong on our end.) y algunos se aceptaban ignorando el elemento (en las facturas, propertyInfo, parts y customsInfo de los conceptos se timbraban sin él; en la cancelación por referencias, taxCredentials se ignoraba); taxCredentials, paidInvoices, paidInvoiceTaxes y ubicaciones, que ya respondían 400 con otro texto (la falla del par de certificados y no debe estar vacío., no debe estar vacío. o el mínimo de 2 ubicaciones), responden ahora solo esta falla. Las listas de textos conservan su regla y metadata (un diccionario) sigue aceptando valores null. El SDK no agrega elementos null por su cuenta: solo se envían si su código los pone en una lista.
  • Certificados y llaves privadas: una contraseña que no abre la llave privada (.key) responde 400 de validación con la falla en el campo de la contraseña y el mensaje La contraseña de la llave privada (.key) es incorrecta o el archivo no es una llave privada. (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo), antes de llamar al PAC y sin consumir timbre: en getInvoiceService().create() con el emisor por valores (taxCredentials del emisor), en Issuer.TaxCredentials[n].Password (n = posición de la llave, desde 0); en getTaxFileService().create() con una llave privada (fileType 1 o 3) y en getManifestService().sign(), en Password. El mismo mensaje sale si el archivo enviado como llave no es una llave privada. Antes respondía 400 sin falla de validación, con The .KEY's password is incorrect. La cancelación por valores (getInvoiceService().cancel()) no abre la llave: la revisa el PAC, como antes. Un certificado o una llave recibidos que no se pueden leer, en un flujo sin esa revisión (por ejemplo, un .cer ilegible en getManifestService().sign()), responden 400 con No se pudo leer el certificado (.cer) o la llave privada (.key). (en el SDK, getHttpStatusCode() 400 y ese texto en getDetails()) (antes, The .KEY's password is incorrect.). Si el API no puede descifrar un dato protegido que guarda (por ejemplo, la contraseña de los certificados de una persona al timbrar o cancelar por referencias, o en getTaxFileService().getDefaultValues()), responde 500 con Sorry, it's not you, it's me. y No pudimos descifrar un dato protegido del servidor. Escríbenos a soporte@fiscalapi.com. (en el SDK, getHttpStatusCode() 500 y ese texto en getDetails()): es un error del servidor, no de la petición; escriba a soporte. Antes respondía 400 con The .KEY's password is incorrect.
  • Datos de empleador y de empleado: getPersonService().getEmployerService().create() para una persona que ya tiene datos de empleador responde 400 de validación con la falla en PersonId y el mensaje La persona ya tiene datos de empleador: modifícalos con PUT /api/v4/people/{personId}/employer., y getPersonService().getEmployeeService().create() para una persona que ya tiene datos de empleado, con la falla en EmployeePersonId y el mensaje La persona ya tiene datos de empleado: modifícalos con PUT /api/v4/people/{personId}/employee. (en el SDK, getSucceeded() es false y getDetails() trae la falla con el campo); modifíquelos con getPersonService().getEmployerService().update() o getPersonService().getEmployeeService().update(). Antes respondía 400 sin falla de validación, con Sorry, it's not you, it's me. y A record with the same unique values already exists.
  • Decimales de importes, cantidades y tasas (getInvoiceService().create(); también POST /api/v4/invoices/calculate, que este SDK no usa): un número del request de facturación con más decimales significativos de los que admite su atributo en el XML del SAT (los ceros finales no cuentan) responde 400 de validación con Sorry, it's not me, it's you. y One or more validation failures have occurred., una falla por valor en su campo (propertyName con la ruta en PascalCase, por ejemplo Items[0].Discount; attemptedValue es el número recibido) y el mensaje '<campo>' admite como máximo <n> decimales (se recibieron <m>): redondéalo antes de enviarlo; FiscalAPI no lo redondea. (en el SDK, getSucceeded() es false y getDetails() trae las fallas con su campo), antes de llamar al PAC y sin consumir timbre. Por ejemplo, discount 0.30000000000000004 responde en Items[0].Discount 'discount' admite como máximo 6 decimales (se recibieron 17): redondéalo antes de enviarlo; FiscalAPI no lo redondea. Límites: 6 en quantity, unitPrice y discount de los conceptos (y quantity y unitPrice de sus partes), taxRate de sus impuestos, exchangeRate, los importes y tasas del complemento de pagos (exchangeRate, amount, previousBalance, paymentAmount, remainingBalance, subTotal, base y taxRate) y marketPrice y grantPrice de stockOptions; 10 en equivalence; 2 en los importes de nómina (percepciones, horas extra, otros pagos, jubilación, separación, deducciones, incapacidades, salarios del empleado y ownResourceAmount del empleador); 3 en daysPaid y timePercentage. Antes el valor llegaba al PAC, que respondía un error de esquema sin el campo (CFDI40999, CRP20999 o CFDI40117), y en nómina totalInstallments y dailyAmount junto con totalOneTime se ignoraban y la factura se timbraba (ahora también responden el 400). El API no redondea: con BigDecimal el caso típico es new BigDecimal(double) (por ejemplo new BigDecimal(0.1) tiene 55 decimales) o una división; use BigDecimal.valueOf(double) y redondee antes de asignarlo, por ejemplo valor.setScale(6, RoundingMode.HALF_UP) (2 en los importes de nómina). Además, daysPaid con uno o dos decimales o con .0 (por ejemplo 15.5 o 15.0), que el PAC rechazaba con CFDI40999, se timbra: el XML lo escribe entero o con exactamente 3 decimales (15.500, 15). Un importe que va tal cual con ceros de más (por ejemplo 200.0000000) lo sigue rechazando el PAC con CFDI40999. Los campos de Carta Porte y Comercio Exterior conservan su propia validación de decimales.
  • Facturas que antes fallaban y ahora se timbran (getInvoiceService().create(); sin cambio de contrato ni del SDK): (1) nómina: los totales del complemento (TotalPercepciones, TotalDeducciones y TotalOtrosPagos) y el concepto que genera el API se escriben siempre con 2 decimales; antes repetían los decimales con que llegaba cada importe y, si un importe llegaba sin ceros finales, el PAC rechazaba la nómina con CFDI40168 (Descuento del concepto con más decimales que su Importe). En Java pasaba si el BigDecimal no llevaba los ceros: new BigDecimal("3322.2") se envía como 3322.2. El XML y la respuesta llevan ahora, por ejemplo, 5000.00 en lugar de 5000.0 o 5000 (mismo valor). (2) Nómina con datos de empleador o de empleado en blanco ("" o solo espacios), enviados por valores (issuer.employerData, recipient.employeeData) o guardados con getPersonService().getEmployerService().create() y update() o getPersonService().getEmployeeService().create() y update(): curp, employerRegistration y originEmployerTin del empleador, y curp, socialSecurityNumber, seniority, employeeNumber, department, position y bankAccount del empleado se omiten del XML; antes el PAC respondía CFDI40999. Si el tipo de contrato (01 a 08) exige el registro patronal, ahora responde el error fiscal del PAC (NOM42). (3) Factura con descuento: un concepto cuyo discount tiene más decimales que su importe (por ejemplo unitPrice 200 con discount 10.55) se timbra: el XML escribe el Importe con los decimales del Descuento (200.00), hasta 6; antes el PAC respondía CFDI40168. (4) Nómina con una percepción 045 (acciones o títulos) con stockOptions (marketPrice y grantPrice): antes respondía 500 Something went wrong on our end. (en el SDK, getHttpStatusCode() 500) sin llamar al PAC ni consumir timbre; ahora se timbra con el nodo AccionesOTitulos (ValorMercado y PrecioAlOtorgarse) y la factura se guarda con él.
  • Errores del servidor (todos los métodos): un error inesperado del API (500) deja de traer en details el texto interno del error (por ejemplo, de la base de datos o de la conversión de datos del servidor), que queda solo en los registros de Fiscalapi con el mismo traceIdentifier: responde Something went wrong on our end. con Ocurrió un error inesperado en el servidor. Escríbenos a soporte@fiscalapi.com con el traceIdentifier de esta respuesta. (en el SDK, getHttpStatusCode() 500, ese texto en getDetails() y el identificador en getTraceIdentifier()). El status y message no cambian, ni los 500 con texto propio (por ejemplo, el del dato protegido que el API no puede descifrar; ver la nota de certificados y llaves privadas). message indica además de qué lado está la causa: todo 5xx trae Sorry, it's not you, it's me., salvo el 500 inesperado (Something went wrong on our end.), y todo 4xx Sorry, it's not me, it's you.. Antes, los 5xx propios del API con un texto en details traían Sorry, it's not me, it's you.; por ejemplo, el 503 de getInvoiceService().send() cuando el proveedor de correo no está disponible y el 502 de getDownloadRequestService().create() cuando el SAT no pudo crear la solicitud (en el SDK, getMessage() pasa a Sorry, it's not you, it's me.; details y el status no cambian). Conservan su propio message el rechazo del PAC al timbrar o cancelar, el 425 (Too Early) de la descarga de paquetes y el 429 (Too Many Requests) del límite de solicitudes. Base de datos: si la base de datos de Fiscalapi no respondió a tiempo o no está disponible, el API responde 503 con Sorry, it's not you, it's me. y La base de datos no respondió a tiempo o no está disponible. Intenta de nuevo en unos minutos; si continúa, escríbenos a soporte@fiscalapi.com con el traceIdentifier de esta respuesta. (reintente más tarde; antes, 400 con The server did not respond in time. Please try again later. al guardar o el 500 inesperado al consultar); si otra petición modificó o eliminó el registro entre su lectura y su escritura, 409 con Sorry, it's not me, it's you. y Otra petición modificó o eliminó el registro después de leerlo. Vuelve a consultarlo e inténtalo de nuevo. (antes 400 con An error occurred while processing your request.; el 409 de personas no cambia); y una solicitud que no cumple una restricción de los datos de la base, 400 con Sorry, it's not me, it's you. (antes Sorry, it's not you, it's me.), sin falla de validación y con uno de estos textos en details: Ya existe un registro con los mismos datos únicos., La operación no cumple una restricción de los datos: un registro relacionado no existe o depende de este, o un valor no es válido., Falta un dato obligatorio., Uno o más campos exceden la longitud máxima permitida., El formato de uno o más campos es incorrecto. o Uno o más valores numéricos exceden el máximo permitido. (antes en inglés, como A record with the same unique values already exists., o en algunas operaciones el 500 inesperado) (en el SDK, getHttpStatusCode() 503, 409 o 400 y el texto en getDetails()). Cualquier otra falla de la base de datos es el 500 inesperado.
  • Carta manifiesto (getManifestService().sign()): todas las personas del tenant con el RFC del certificado quedan con Person.getManifestStatusId() en Signed; antes, si varias personas del tenant compartían ese RFC, se marcaba solo una de ellas. La petición y la respuesta no cambian; si ninguna persona del tenant tiene ese RFC, sigue respondiendo 404 con No existe un usuario con RFC <rfc> en el tenant actual.

Ejemplos

  • README y Examples.java sin setCapitalRegime.