Cambios del SDK y notas de comportamiento del API de FiscalAPI que afectan a quien usa el SDK.
- Cambio del API, no del SDK: cancelar factura (
InvoiceService.cancel) frente a un rechazo del SAT responde 422 conisSucceeded()enfalse. Antes respondía 200 conisSucceeded()entruey el código 203 engetData().getInvoiceUuids(). El SDK no lanza excepción. getData()sigue con el acuse (InvoiceCancellationResponse) ygetErrorData()con el JSON crudo: solo cambian el status (422) yisSucceeded(). El código del SAT está engetDetails().- Migración: trate el 422 como rechazo; quien filtraba por
getHttpStatusCode()200 o porisSucceeded()cambia de rama. - Las validaciones previas al PAC responden 400: UUID no canónico, sustituta inválida con motivo 01, y cancelar por
iduna 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.
WebhookEventData.getReason()(nuevo,String): el motivo de la transición. Todo evento lo trae; esnullsalvo eninvoice.cancellation_rejected, donde dice por qué no procedió la cancelación:rejectedByRecipient,expired,notRequestedonotCancelable. Es texto libre: pueden aparecer valores nuevos. Cambio aditivo: nada existente cambia ni deja de compilar.InvoiceService.cancelfrente a un rechazo del SAT: el API responde 422 y el SDK no lanza.isSucceeded()esfalse,getMessage()es el mensaje genérico,getDetails()traeSAT <código>: <descripción>y el acuse llega tipado engetData()(InvoiceCancellationResponse:getBase64CancellationAcknowledgement()egetInvoiceUuids()con el código del SAT por UUID) y crudo engetErrorData(). El SDK no cambia de código: el cambio es del API (ver Cambios incompatibles).
- Jackson sube de 2.14.2 a 2.18.12 (vía
jackson-bom), que corrige avisos de seguridad dejackson-databindyjackson-core(p. ej. GHSA-h46c-h94j-95f3). Sin cambios de API.
getForeignBankName()ysetForeignBankName(String)(foreignBankName, opcional): nombre del banco ordenante extranjero, el atributoNomBancoOrdExtdel complemento de pago. El SAT lo exige cuandosourceBankTinesXEXX010101000(CRP20223); antes el SDK no podía enviarlo. Sin asignarlo no se envía, así que el código existente manda el mismo JSON.
- Llave de idempotencia opcional con el header
Idempotency-Keyen la creación de facturas y en la transferencia de timbres:getInvoiceService().create(invoice, idempotencyKey)(POST /api/v4/invoices) ygetStampService().transferStamps(params, idempotencyKey)(POST /api/v4/stamps). Con una llave de idempotencia distinta de null envían el header;create(invoice)ytransferStamps(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
IInvoiceServiceoIStampService(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) conIdempotent-Replayed: trueen 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.
Webhook.verify(headers, body, secret)yWebhook.verify(headers, body, secret, tolerance)(paquetecom.fiscalapi.webhooks) verifican una entrega de webhook de FiscalAPI y devuelven suWebhookEvent: validan la firma Standard Webhooks (HMAC-SHA256 sobrewebhook-id.webhook-timestamp.cuerpocon el secretowhsec_…, comparada en tiempo constante) y quewebhook-timestampno difiera del reloj del receptor más que la tolerancia (java.time.Duration), hacia atrás o hacia adelante (por omisiónWebhook.DEFAULT_TOLERANCE, 5 minutos). Aceptan cualquier firmav1válida del headerwebhook-signature(varias, separadas por espacio) y el secreto con o sin el prefijowhsec_.bodyes el texto (String) o los bytes (byte[]) del cuerpo; conbyte[]se lee como UTF-8 estricto. Las sobrecargasverify(headers, body, List<String> secrets)yverify(headers, body, List<String> secrets, tolerance)aceptan varios secretos para la rotación (FiscalAPI firma con el nuevo y el anterior durante 24 h).headerses unMap<String, String>o unMap<String, List<String>>(servlet,@RequestHeaderoMultiValueMapde 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 averify. - 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, extiendeRuntimeException); 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 lanzanIllegalArgumentException(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
ObjectMapperdel cliente HTTP ni uno de la aplicación, acepta campos desconocidos, normalizacreatedAta UTC sin importar la zona de la JVM y rechaza contenido después del evento.
- 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
- Modelo
WebhookEvent(paquetecom.fiscalapi.models.webhooks), el mismo sobre para todo evento:getId()(igual al headerwebhook-id, se repite en cada reintento),getType(),getCreatedAt()(OffsetDateTimeen UTC),getPersonId()(null si el recurso no tiene persona) ygetData()(WebhookEventData) congetRelatedObject()(WebhookRelatedObject:getId(),getType(),getUrl()),getStatus()ygetPreviousStatus().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 ygetRelatedObject().getId(). getType()es texto y no un enum: las constantes deWebhookEventTypes(INVOICE_CANCELLATION_REQUESTED,INVOICE_CANCELED,INVOICE_CANCELLATION_REJECTED,DOWNLOAD_REQUEST_COMPLETED,DOWNLOAD_REQUEST_ABANDONED,DOWNLOAD_RULE_SUSPENDEDyWEBHOOK_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), conrelatedObject.typewebhookyrelatedObject.url/api/v4/webhooks/{id}, sin persona ni estados.- Cambio aditivo: nada existente cambia ni deja de compilar, y no hay dependencias nuevas.
- Tipo de crédito
TICKET(3) para transferir y leer créditos de ticket:getStampService().transferStamps()consetCreditType(CreditType.TICKET)los transfiere con las mismas reglas que los timbres, y las transacciones (getStampService().getList(),getStampService().getById()) los devuelven comoCreditType.TICKET. El saldo de créditos de ticket se lee engetBalances()de la persona (elCreditBalancecongetCreditType()igual aCreditType.TICKET.getValue()); un tipo ausente tiene saldo 0. Los valores existentes no cambian (STAMP1,VALIDATION2).
- Las transacciones con un tipo de crédito, tipo o estatus desconocido conservan su número en
get*Value()y al serializar: los nuevosgetCreditTypeValue(),getTransactionTypeValue()ygetTransactionStatusValue()devuelven el entero que envió la API (Integer), ytoString()lo vuelve a escribir tal cual. Antes el número se perdía.getCreditType(),getTransactionType()ygetTransactionStatus()no cambian: con un número que el SDK no conoce devuelvennull, como antes.
- Servicio de tickets (Auto CFDI):
tickets.create(conIdempotency-Keyopcional),getListcon filtros,getById,getCodeycancel, engetTicketService()(ITicketService/TicketService):create(ticket)ycreate(ticket, idempotencyKey):POST /api/v4/tickets; cobra 1 crédito de ticket y devuelveTicketIngestionResult(id,ticketCode,qrUrl,status,selfInvoiceEligible,eligibleUntily los importes calculados). ConidempotencyKeydistinto de null envía el headerIdempotency-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, comoApiResponsefallida con el texto de la API engetDetails()y sudataengetErrorData().getList(TicketListFilters):GET /api/v4/ticketsconstatus(List<TicketStatus>; se repite la llavestatus),personId,details(contrueenvíadetails=true: cada ticket trae sus conceptos con impuestos, sin la historia;nullofalseno se envían),globalInvoiceId(factura global que ampara los tickets),from/to(LocalDate, fecha civil de México),pageNumberypageSize; los filtros en null no se envían.getById(id, details): condetailstraeitems(conitemTaxes) yevents.getCode(id)devuelveTicketCodeResult(ticketCode,qrUrl) para reimprimir.cancel(id)usaDELETE(solo desdepending; el crédito no se devuelve).
- Modelos nuevos en
com.fiscalapi.models.tickets:Ticket,TicketIssuer,TicketItem,TicketItemTax,TicketEvent,TicketIngestionResult,TicketCodeResultyTicketListFilters.paymentDateesLocalDateTimeen hora civil de México y viaja comoyyyy-MM-ddTHH:mm:ss; los importes (BigDecimal) se envían como texto con su escala (constrúyalos desde un String). - Enums
TicketStatus,TicketEventTypeyTicketActorType: los modelos guardan el texto de la API;getStatus(),getType()ygetActorType()devuelven null con un valor que el SDK no conoce, ygetStatusValue(),getTypeValue()ygetActorTypeValue()conservan el texto. IFiscalApiClientagregagetTicketService(). Quien implementa su propia versión deIFiscalApiClient(p. ej. un doble de pruebas) debe agregargetTicketService(); quien usa el cliente del SDK no cambia nada.IFiscalApiHttpClientagrega la sobrecargapost(url, body, responseType, headers)para headers propios de la petición (la usaIdempotency-Key). Quien implementa su propia versión deIFiscalApiHttpClient(p. ej. un doble de pruebas) debe agregarpost(url, body, responseType, headers); quien usa el cliente del SDK no cambia nada.- Ejemplo:
examples/EjemplosTickets.java.
ticketIden el concepto de la factura para ligar la factura global a los tickets (Auto CFDI); se omite si es nulo. En Java esInvoiceItem.getTicketId()/setTicketId(String), con@JsonInclude(NON_NULL)en el campo: ennullno viaja en el JSON, aunque elObjectMapperincluya los nulos. Un 409 por tickets no disponibles traeunavailableTickets(ticketId,status) engetErrorData(). Ejemplo:examples/EjemplosFacturaGlobalTickets.java.
getErrorData()(JsonNodede Jackson): nuevo miembro (aditivo) con eldatacrudo de una respuesta de error, tal como lo envía la API: por ejemploexistingTicketIden el 409 de la ingesta de tickets (response.getErrorData().get("existingTicketId").asText()) oexpectedTotalycomputedTotalen su 422. Es null en las respuestas exitosas y en los errores sindata.getData(),getMessage(),getDetails(),getHttpStatusCode()ygetTraceIdentifier()no cambian.
Payroll.daysPaidpasa deintaBigDecimal(getDaysPaid()devuelveBigDecimalysetDaysPaid(BigDecimal)lo recibe): el API y el SAT admiten días pagados fraccionarios (NumDiasPagados, entero o con hasta 3 decimales, por ejemplo15.5) y conintel SDK no podía enviarlos. El código que llamasetDaysPaid(15)o guardagetDaysPaid()en unintdeja de compilar: usesetDaysPaid(new BigDecimal("15"))oBigDecimal.valueOf(15.5)(nonew BigDecimal(15.5)con undoubleque 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.5viaja como"15.5", que el API timbra como15.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 viajaba0); el API lo toma como0, igual que antes.- Se eliminan
Person.getCapitalRegime()yPerson.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 consetLegalName. - El API deja de devolver
stripeCustomerIdysubscriptionStatusen las personas (/api/v4/peopley la persona de las reglas de descarga): eran datos internos de Stripe. Este SDK nunca los modeló enPerson(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,expYeary 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 elimport com.fiscalapi.common.StripePaymentMethodDto;y esos usos; el API no expone métodos de pago ni datos de Stripe que la reemplacen. - Se elimina
updatedel 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 congetTaxFileService().create()y elimine el anterior congetTaxFileService().delete(). El método ya no existe ni en la interfaz ni en la clase del servicio:ITaxFileServiceextiende la nuevaIImmutableFiscalApiService<TaxFile>(consulta, creación y baja, sinupdate()) yTaxFileServicehereda de la nuevaBaseImmutableFiscalApiService<TaxFile>, así queclient.getTaxFileService().update(...)deja de compilar (cannot find symbol) y el servicio deja de ser unIFiscalApiService<TaxFile>. Los demás servicios no cambian:IFiscalApiService<T>extiendeIImmutableFiscalApiService<T>conupdate()yBaseFiscalApiService<T>hereda deBaseImmutableFiscalApiService<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 degetTaxFileService().create()traenpasswordvacía (""); antes, la contraseña en claro (tambiénGET /api/v4/tax-files/allyGET /api/v4/tax-files/load, que este SDK no usa). SologetTaxFileService().getDefaultValues()la devuelve (a la persona misma o al owner del tenant);getTaxFileService().getDefaultReferences()ya la devolvía vacía. El modeloTaxFileno cambia (su Javadoc lo documenta): la contraseña solo se envía al subir congetTaxFileService().create(). Si su integración leeTaxFile.getPassword()de una de esas consultas, usegetTaxFileService().getDefaultValues()o la contraseña que ya conoce. Ningún método ni ejemplo del SDK lee la contraseña de una consulta.
- 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:creditTypeyavailable, solo lectura). Solo aparecen los tipos que la persona ha tenido; un tipo ausente tiene saldo 0, ycreditTypeesIntegerpara conservar un tipo que el SDK no conoce.isOwner:truesi 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 esnully es informativa (no limita el timbrado ni el acceso al API).
password: requerida al crear; al actualizar,nullo 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,stripeCustomerIdysubscriptionStatus, que el API ya no devuelve (el comentadovalidTopasa a ser miembro real).
EmployerData.curp(getCurp()/setCurp(String)): nuevo miembro con la CURP del empleador persona física (Nomina/Emisor/@Curp), que se envía comoissuer.employerData.curpen 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í).
ApiResponse.getDetails()en un 400 de validación une todas las fallas comoCampo: mensajeseparadas por;(por ejemploItems[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 degetDetails(), compare el campo de cada falla en su lugar.
DownloadRequest.getNextAttemptDate()ysetNextAttemptDate()quedan obsoletos (@Deprecated) y se eliminarán en una versión mayor. El API ya no devuelvenextAttemptDateen las solicitudes de descarga (getDownloadRequestService().getList(),getDownloadRequestService().getById(),getDownloadRequestService().search()ygetDownloadRequestService().create()): siempre eranull, porque Fiscalapi no programa un siguiente intento por solicitud (revisa las solicitudes pendientes periódicamente). Los métodos siguen existiendo ygetNextAttemptDate()devuelvenull, como antes; el código que los usa compila con una advertencia de obsolescencia: quite esa lectura (la fecha del último intento está engetLastAttemptDate()).
Person.taxPasswordes 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 detaxCredentials). Solo la reciben con valor la propia persona y el owner del tenant; los demás recibennull. Al actualizar,nullla conserva (el SDK no envía losnull) y""la borra.ValidationFailure.attemptedValue(y el placeholderPropertyValuedeformattedMessagePlaceholderValues): 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]"(nes su longitud), o"[masked]"si la falla es de un objeto o una lista que lo contiene.TaxCredential.passwordsolo se exige en la llave privada (.key); en el certificado (.cer) el API no la usa.TaxFile.tines opcional engetTaxFileService().create(): si esnullo 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 enTin(después de comprobar que puede gestionar los certificados de la persona; si no, 403). El API no guarda el valor enviado: eltindel archivo siempre es el RFC de la persona.- Las personas ya no traen
stripeCustomerIdnisubscriptionStatus(datos internos de Stripe).validToycommittedBalanceson de solo lectura: el API los ignora al crear o actualizar.committedBalancees un campo heredado que el API ya no calcula y siempre vale 0; para el saldo useavailableBalance,availableValidationBalanceobalances. - Personas:
getPersonService().create()ygetPersonService().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 enEmaily el mensajeThe email is already registered.(en el SDK,getSucceeded()esfalseygetDetails()vale"Email: The email is already registered."). Antes respondía otro 400 (una falla de Identity o el genéricoA 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()ygetPersonService().delete()pueden responder 409 condetailsThe 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 engetDetails()) 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 engetApiKeyService().getList()ni engetApiKeyService().getById()(responde 404 conNo such resource for the given Id: <id>); antes seguían en el listado como habilitadas (apiKeyStatus1) y sin persona. - Personas:
getPersonService().create()ygetPersonService().update()validan el correo y la contraseña con las reglas de Identity antes de guardar. Un correo con caracteres fuera dea-z,A-Z,0-9y-._@+(por ejemploñ, una vocal acentuada, un apóstrofo o un espacio intermedio) responde 400 de validación con la falla enEmail,errorCodeInvalidUserNamey el mensajeThe 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úsculaA-Z, una minúsculaa-z, un dígito0-9y 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 enPasswordcon el mensajePassword 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 enerrorCode(p. ej.PasswordTooShortoPasswordRequiresUpper) yattemptedValuenull(en el SDK,getSucceeded()esfalseygetDetails()trae la falla con el campo). Antes, al crear, esos valores llegaban a Identity y la falla traía el código enpropertyName(InvalidUserName,PasswordRequiresUpper…) con el texto de Identity (p. ej.Username '…' is invalid, can only contain letters or digits.) (en el SDK,getDetails()empezaba conInvalidUserName:y ahora conEmail:); una contraseña corta comoabcdaba dos fallas (MinimumLengthValidatoryPredicateValidator), 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 enpropertyNameel campo del request (EmailoPassword) y el código de Identity enerrorCode. - Facturas (
getInvoiceService().create()): con una moneda extranjera (currencyCodedistinto deMXNy deXXX),exchangeRate0 o negativo responde 400 de validación con la falla enExchangeRatey el mensajeCon 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()esfalseygetDetails()trae la falla con el campo); antes pasaba la validación del API. ConMXNsigue siendo exactamente 1. El complemento de pagos sigue la misma regla en el tipo de cambio del pago (complement.payment.exchangeRatefrente acomplement.payment.currencyCode): conMXN, exactamente 1 (si esnull, el API usa 1); con otra moneda, mayor que 0. Si no, responde 400 de validación con la falla enComplement.Payment.ExchangeRate(mensajeCon 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/registeryPOST /api/v4/account/infocon un correo que ya pertenece a otra persona responden 400 de validación con la falla enEmail(NewEmaileninfo) y el mensajeThe email is already registered.;POST /api/v4/account/change-passwordcon la contraseña actual incorrecta responde 400 de validación con la falla enCurrentPassword(errorCodePasswordMismatch, mensajeIncorrect password.); en general, las fallas que vienen de Identity enaccount/change-password,account/info,account/reset-passwordyaccount/confirm-emailtraen enpropertyNameel campo del request que las causó (CurrentPassword,NewPassword,NewEmail,ResetCodeoCode) y el código de Identity enerrorCode(PasswordMismatch,InvalidToken, …), en lugar del código comopropertyName;POST /api/v4/account/refreshcon un refresh token vencido responde 401Invalid refresh tokenen lugar de emitir otro (con uno vigente devuelve el mismo token), así que una sesión iniciada conaccount/loginsolo se puede renovar hasta 7 días después del login. - Facturas (
getInvoiceService().create()): concurrencyCodeXXX(facturas de pago y de traslado), unexchangeRatedistinto de 1 y de 0 responde 400 de validación con la falla enExchangeRatey el mensajeCon la moneda XXX, 'exchangeRate' (tipo de cambio) no aplica: se omite (o se envía 1 o 0).(en el SDK,getSucceeded()esfalseygetDetails()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 esnull, el API usa 1) o 0 se sigue timbrando, sinTipoCambioen el XML. - Facturas de pago (
getInvoiceService().create()concomplement.payment): la moneda del pago (complement.payment.currencyCode) y la de cada factura pagada (currencyCodede cadaPaidInvoice) 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, comomxn) o sonXXX, responde 400 de validación con la falla enComplement.Payment.CurrencyCodeo enComplement.Payment.PaidInvoices[n].CurrencyCode(n= posición, desde 0) con el mensajeLa 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()esfalseygetDetails()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 serXXX: con otra, 400 de validación enCurrencyCodeconLa moneda de un comprobante de pago debe ser XXX.(antes, el PAC respondía CRP20104). El valor por omisión deInvoice.currencyCodeesMXN: en una factura de pago asígneleXXX. Una factura sincurrencyCoderesponde una sola falla enCurrencyCode(antes, dos). - Factura global (
getInvoiceService().create()conglobalInformation.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 enGlobalInformation.Yeary 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()esfalseygetDetails()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-passwordyPOST /api/v4/account/reset-password(NewPassword) aplican las mismas reglas de correo y contraseña que las personas (400 conInvalidUserNameo con la falla única de la contraseña); antesregistercreaba la cuenta con un correo conño una contraseña comoÑandú12!.reset-passwordvalida la contraseña nueva antes del código (con las dos inválidas, la falla es la deNewPassword). Enaccount/info,CurrentPasswordya no se valida con las reglas de formato (una incorrecta respondePasswordMismatchenCurrentPassword) y es obligatoria cuando vieneNewPassword: sin ella, 400 de validación enCurrentPasswordconCurrent password is required to set a new password.(antes, el mismo texto en un 400 sin campo). Restablecimiento de contraseña: el código quePOST /api/v4/account/forgot-passwordenvía por correo (elresetCodedePOST /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). UnresetCodeque no es de 6 dígitos responde 400 de validación enResetCodeconEl 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 enResetCode(errorCodeInvalidToken) conEl código no es válido o ya venció.(antesInvalid token.; el mismo texto enCodedeaccount/confirm-email, cuyo enlace no cambia).forgot-passwordrespondeSi el correo está registrado y confirmado, te enviamos un código de 6 dígitos para restablecer tu contraseña; vence en 15 minutos.(antesPassword reset instructions have been sent if the email is registered and confirmed.) yreset-password,La contraseña se restableció correctamente.(antesPassword 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 contrue, o 400 confalsesi no existe, ya se había eliminado o es de otra persona. Para volver a usar un certificado eliminado, súbalo de nuevo congetTaxFileService().create(). - Facturas (
getInvoiceService().create()): con el RFC genérico (XAXX010101000oXEXX010101000) el régimen fiscal del receptor debe ser616y su código postal el deexpeditionZipCode; con el régimen616el uso CFDI debe serS01oCP01; y una factura de ingreso (I) aXAXX010101000PUBLICO EN GENERALdebe traerglobalInformation. Si no, responde 400 de validación (en el SDK,getSucceeded()esfalseygetDetails()trae la falla con el campo), con todas las fallas juntas, antes de timbrar y sin consumir timbre, enRecipient.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).) oGlobalInformation(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()ygetInvoiceService().getById()traeInvoice.getGlobalInformation(): completa (getPeriodicityCode(),getMonthCode()ygetYear()) en una factura global, onullsi 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 ennull). - Facturas (
getInvoiceService().create()): el tipo de factura y sus complementos deben corresponder, como exige el SAT:Iadmitecomplement.localTaxes,complement.cartaPorteycomplement.comercioExterior(o ninguno);E,complement.localTaxesycomplement.comercioExterior(o ninguno);Trequierecomplement.cartaPorteocomplement.comercioExterior;Prequierecomplement.paymentyN,complement.payroll;T,PyNno admiten otro complemento. Si falta el complemento que el tipo requiere o viene uno que no le corresponde (por ejemplopayrollen un ingreso,cartaPorteen un pago olocalTaxesen un traslado), responde 400 de validación con la falla enComplement(en el SDK,getSucceeded()esfalseygetDetails()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.oUn comprobante de tipo N (nómina) requiere complement.payroll y no admite otro complemento.EnIyE, uncomplementvacío (sin complementos, o con todos ennull) equivale a no enviarlo; enT,PyN, responde el 400 de este tipo. Antes, sin el complemento que el tipo requiere o con uncomplementvací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 conpaymentfuera dePo CP114 concartaPorteenPoE), o el complemento sobrante se omitía sin aviso (por ejemplolocalTaxesen un pago), y unIconpayrollse enviaba como nómina (N).POST /api/v4/invoices/calculate, que este SDK no usa, responde igual, y con untypeCodefuera del catálogo responde 400 enTypeCode(antes, 500). - Facturas (
getInvoiceService().create()): el 400 de untypeCodefuera del catálogo diceEl tipo de comprobante '<valor>' no existe en el catálogo c_TipoDeComprobante del SAT.(antesPlease use the SAT catalog values: I,E,T,P,N,P, con laPrepetida; 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()esfalseygetDetails()trae la falla con el campo) en lugar de llegar al PAC como0001-01-01(que la rechazaba con CFDI40101 o CFDI40999):date→Datecon'date' es obligatorio y debe ser una fecha con el formato yyyy-MM-ddTHH:mm:ss.;complement.payment.paymentDate→Complement.Payment.PaymentDatecon'paymentDate' es obligatorio y debe ser una fecha con el formato yyyy-MM-ddTHH:mm:ss.; ycomplement.payroll.paymentDate,initialPaymentDateyfinalPaymentDate→Complement.Payroll.PaymentDate,Complement.Payroll.InitialPaymentDateyComplement.Payroll.FinalPaymentDatecon'<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 facturaI,EoTsinitems(o con la lista vacía) responde 400 de validación con la falla enItemsy el mensajeitems debe tener al menos un concepto en los comprobantes de tipo I, E y T.(en el SDK,getSucceeded()esfalseygetDetails()trae la falla con el campo), sin ir al PAC; antes la rechazaba el PAC (CFDI40999 o CP999: el nodoConceptoses obligatorio en el CFDI). EnPyNse 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 (todoTyP, o un ingreso con descuento del 100 %), responde 200 con el cálculo (antes, 400 consucceededfalse). - 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, enComplement.CartaPorte.…oComplement.ComercioExterior.…(por ejemploComplement.CartaPorte.Ubicaciones[0].NumEstacionIdconNumEstacionId must be a valid c_Estaciones value.) (en el SDK,getSucceeded()esfalseygetDetails()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 enComplement.CartaPorte.Ubicaciones[n].FechaHoraSalidaLlegadacon'fechaHoraSalidaLlegada' es obligatorio y debe ser una fecha con el formato yyyy-MM-ddTHH:mm:ss.(antes llegaba al PAC como0001-01-01). Las claves de estación (c_Estaciones) se escriben como en el catálogo del SAT, con acento cuando lo llevan:Juárez0se acepta yJuarez0responde 400. ElNumRegIdTribdel 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
nulldentro de un complemento, por ejemploComplement.CartaPorte.TranspInternacIdcon'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, antesgetMessage()llegaba vacío ygetDetails()ennull(el SDK no lee ProblemDetails); ahoragetSucceeded()esfalse,getMessage()esSorry, it's not me, it's you.ygetDetails()trae la falla comoCampo: mensaje. En esas fallaserrorCodeesnull. 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 onull, en$conEl cuerpo de la petición es requerido y no debe estar vacío.El ProblemDetails de antes traía además enerrorsla 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
errorMessagegenerales 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.(antesThe TranspInternacId field is required.),El cuerpo de la petición es requerido y no debe estar vacío.(antesA non-empty request body is required.) yEl valor 'abc' no es válido para PageNumber.(antesThe value 'abc' is not valid for PageNumber.). En el SDK cambia el texto degetDetails()(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) ygetInvoiceService().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 conNo se encontró la llave privada de la persona: <id>oNo se encontró el certificado de la persona: <id>(en el SDK,getHttpStatusCode()404 y ese texto engetDetails()); 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 conAmbos 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 conNo 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 conNo se proporcionaron archivos fiscales para crear un credencial FIEL o CSD.y la cancelación 400 en inglés conWhen 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 elissuer.idenviado; en la cancelación, el id del emisor de la factura. - Certificados:
getTaxFileService().getDefaultReferences()aplica la misma regla quegetTaxFileService().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 conSolo 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 engetDetails()); 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):PUTyDELETEsobre la regla predeterminada de FiscalAPI (compartida con todas las cuentas) responden 403 conOperation denied.(en el SDK,getHttpStatusCode()403 y ese texto engetDetails()); antes el API permitía modificarla o borrarla.DELETEde un id que no existe responde 404 (antes, 400 confalse). - Facturas por referencias (
getInvoiceService().create()conissuer.id,recipient.idoitems[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>).oEl producto '<id>' no tiene <dato> (<campo>)., por ejemploLa persona emisora '<id>' no tiene RFC (tin).oLa persona receptora '<id>' no tiene uso de CFDI (satCfdiUseId).(en el SDK,getHttpStatusCode()400 y ese texto engetDetails()). En las personas el dato esRFC (tin),razón social (legalName),régimen fiscal (satTaxRegimeId)ocódigo postal (zipCode), y en la receptora tambiénuso de CFDI (satCfdiUseId); en el producto,clave de producto o servicio (satProductCodeId),clave de unidad (satUnitMeasurementId),objeto de impuesto (satTaxObjectId)odescripció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 requiredyProduct <Campo> is required. El código 400 y la forma del error no cambian; un id que no existe sigue respondiendo 404 conNo such resource for the given Id: <id>. - Cancelación por referencias (
getInvoiceService().cancel()conid): con el id de una factura que no existe o que es de otra cuenta, el API responde 404 conNo such resource for the given Id: <id>(en el SDK,getHttpStatusCode()404 y ese texto engetDetails()); 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 conNo 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 conWhen issuer data is missing, issuer's Id must be provided.): cancélela por valores coninvoiceUuid,tinytaxCredentials. Si la persona emisora no tiene RFC, responde 400 conLa persona emisora '<id>' no tiene RFC (tin).(antesLa persona '<id>' no tiene datos fiscales). - Reglas de descarga (
getDownloadRuleService().create(),getDownloadRuleService().update()ygetDownloadRuleService().delete()): solo el owner del tenant crea, actualiza o elimina una regla de la persona owner (personIddel owner). Con la API key de otra persona del tenant, el API responde 403 conSolo el propietario de la cuenta puede modificar al propietario de la cuenta.(en el SDK,getHttpStatusCode()403 y ese texto engetDetails()) 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()) ygetDownloadRequestService().getMetadataItems()(GET /api/v4/download-requests/{id}/meta-items,MetadataItem.getInvoiceDate(),MetadataItem.getSatCertificationDate()yMetadataItem.getCancellationDate()) devuelven ahora la fecha y hora del CFDI/SAT tal cual (la hora local del lugar de expedición, igual a laFechadel XML, y la de la metadata del SAT), sin depender deX-TIME-ZONE(FiscalApiSettings.setTimeZone()); antes se les restaba el desfase de esa zona (6 h conAmerica/Mexico_City, la predeterminada del SDK). Igual enGET /api/v4/download-xmls/{id}yGET /api/v4/download-meta-items/{id}, que este SDK no usa. Ejemplo: XML conFecha="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()ygetLastAttemptDate()) 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()ygetDownloadRequestService().downloadSatResponse()responden 404 conNo such resource for the given Id: <id>(en el SDK,getHttpStatusCode()404 y ese texto engetDetails()), la misma respuesta que ya daban otras rutas por id (por ejemplogetInvoiceService().send()); elmessageesSorry, 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()ygetDownloadRequestService().downloadSatResponse(),No such resource for given id: <id>;getPersonService().getById()ygetPersonService().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; ygetDownloadRequestService().downloadPackage(),No existe una solicitud de descarga con id <id>.con elmessageNot Found.getInvoiceService().getPdf()ygetPersonService().update()respondían 400 (conNo such resource for given id: <id>yNo such resource for the given id: <id>); ahora, 404.PATCH /api/v4/people/{id}, que este SDK no usa, responde igual quegetPersonService().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 conNo such resource for the given Id: <id>(en el SDK,getHttpStatusCode()404 y ese texto engetDetails()); antes respondía 400 conRecord not found for <catálogo> with ID <id>enmessageydetailsvací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()ygetProductService().update(),getPersonService().getEmployeeService().create()y.update(),getPersonService().getEmployerService().create()y.update()):unitPricede un producto debe ser mayor que 0 y como máximo999999999999.999999(12 enteros y 6 decimales); por encima, 400 de validación enUnitPriceconEl precio unitario no puede ser mayor a 999999999999.999999.(antes, 500). Las tasas o cuotas deproductTaxes(rate) deben ser mayores o iguales a 0 y como máximo999999999999.999999(una cuota fija puede ser mayor que 1): 400 enProductTaxes[n].RateconLa tasa o cuota debe ser mayor o igual a 0.oLa tasa o cuota no puede ser mayor a 999999999999.999999.(antes, una tasa negativa se guardaba y una mayor respondía 500).baseSalaryForContributionseintegratedDailySalarydel empleado, yownResourceAmountdel empleador, deben ser mayores o iguales a 0 (el último, si se envía) y como máximo999999999999.999999: 400 en el campo conSalarioBaseCotApor debe ser mayor o igual a 0.,SalarioDiarioIntegrado no puede ser mayor a 999999999999.999999.oMontoRecursoPropio no puede ser mayor a 999999999999.999999.(antes, un salario negativo se guardaba y uno mayor, igual que unownResourceAmountmayor, respondía 500). UnnullenownResourceAmountno se valida. - Productos (
getProductService().create()ygetProductService().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()esfalseygetDetails()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.; yProductTaxes[n].TaxFlagId,La naturaleza del impuesto '<valor>' no es válida: usa T (traslado) o R (retención).Una clave vacía (salvoSatProductCodeId) responde el mensaje de campo vacío. Antes, unSatProductCodeIdo un impuesto inválido respondía 400 sin campo, conSorry, it's not you, it's me.yThe 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>.SatProductCodeIdennullsigue permitido. Un impuesto repetido en la lista de impuestos del producto (mismotaxId,taxTypeId,taxFlagIdyrate; la tasa se compara con 6 decimales, así que0.16y0.1600004son el mismo) responde 400 de validación antes de guardar, con la falla enProductTaxes[n](n= la segunda aparición) y el mensajeEl 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ónYa 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()ygetPersonService().update(); tambiénPATCH /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()esfalseygetDetails()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.; yUserTypeId,UserTypeId debe ser uno de: C, U. 'T' está reservado y solo se permite cuando la persona ya es Tenant.(al actualizar,Tse sigue aceptando si la persona ya esT). Antes, al actualizar, el régimen fiscal y el uso de CFDI respondíanPlease use the SAT catalog values: <lista del catálogo>; al crear, y el país en las dos operaciones, 400 sin campo conSorry, it's not you, it's me.yThe 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, comomxn) responde 400 de validación enCurrencyCodeconLa moneda '<valor>' no existe en el catálogo c_Moneda del SAT.(en el SDK,getSucceeded()esfalseygetDetails()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()ygetDownloadCatalogService().listCatalog()): un nombre de catálogo que no existe responde 404 conNo existe el catálogo '<nombre>'.(en el SDK,getHttpStatusCode()404 y ese texto engetDetails()); elmessageesSorry, it's not me, it's you.Antes respondía 400 conInvalid catalog name: <nombre>enmessage, o un error 500 con algunos nombres (por ejemploDatabaseoUsers). El nombre distingue mayúsculas y minúsculas (satpaymentformsno esSatPaymentForms), como antes; los nombres válidos son los que devuelvenGET /api/v4/catalogsyGET /api/v4/download-catalogs, y responden igual que antes.GET /api/v4/catalogs/<nombre>yGET /api/v4/catalogs/<nombre>/load, que este SDK no usa, responden el mismo 404 (antes, 400 y 500). - Productos (
getProductService().create()ygetProductService().update()): una descripción (description) de más de 255 caracteres responde 400 de validación con la falla enDescriptiony el mensaje'Description' debe ser menor o igual que 255 caracteres. Ingresó <n> caracteres.(en el SDK,getSucceeded()esfalseygetDetails()trae la falla con el campo). Antes, al crear, respondía 400 sin campo conSorry, it's not you, it's me.yThe operation cannot be completed because the record is related to another non-existent record., y al actualizar, 400 conOne 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()esfalseygetDetails()trae la falla con el campo):<El dato> '<valor>' no existe en el catálogo <c_Catálogo> del SAT.enTypeCode(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); enVersionCode,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.oThe 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 ejemploVersionCode,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 unExpeditionZipCodeinvá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 que1000000000000), responde 400 de validación conSorry, it's not me, it's you.yOne or more validation failures have occurred., una falla por valor, sin timbrar ni consumir timbre (en el SDK,getSucceeded()esfalseygetDetails()trae la falla con el campo). El mensaje esAdmite como máximo <n> caracteres y se recibieron <m>.en un texto (attemptedValueennull: puede ser un dato personal) oAdmite como máximo <d> dígitos enteros (valores menores que <10^d>); se recibió <valor>.en un importe o una cantidad (attemptedValuees 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(poritems[0].quantity),InvoicePayment.Amount(porcomplement.payment.amount) oPayroll.Deductions[0].Code(porcomplement.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].ClaveUnidadyComercioExterior.Emisor.Domicilio.Colonia. Antes, un texto más largo lo rechazaba el PAC con su código (por ejemploCFDI40999,CRP20999,CP999,NOM111oCCE999), 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 conSorry, 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,itemSkude 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 atributoUnidaddel concepto y la factura guardada lo devuelve ennull. Puede omitirlo; la unidad del concepto essetUnitOfMeasurementCode()(catálogo c_ClaveUnidad). - Mensajes del timbrado en español (
getInvoiceService().create(); tambiénPOST /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;propertyNamesigue siendo la ruta en PascalCase, por ejemploItems[0].ItemTaxes[0].TaxCode), pero cambia el texto (en el SDK,getSucceeded()esfalseygetDetails()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): enIssuer.TinyRecipient.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.(antesDoes not match the pattern defined in the type defined for expressing Mexican Federal Taxpayer Registry Keys (RFC)); enNumber,'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.; enRelatedInvoices,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).(antesThe 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, contaxTypeCodeCuota,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).; enRecipient.ForeignTin,'foreignTin' (NumRegIdTrib) debe tener de 1 a 40 caracteres.; enRecipient.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 traeUna factura admite como máximo 1500 conceptos (items).; en Carta Porte,'ubicaciones' requiere al menos 2 ubicaciones.ySe 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 ejemploThe 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énPOST /api/v4/invoices/calculate, que este SDK no usa): un total de 2,147,483,648 o más ya no responde 500 conValue 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 diceMILLONESde 2 a 9 millones (antesDOS 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):cancellationReasonCodedebe ser una clave del catálogo c_MotivoCancelacion del SAT (01,02,03o04); otro valor responde 400 de validación con la falla enCancellationReasonCodey el mensajeEl motivo de cancelación '<valor>' no existe en el catálogo c_MotivoCancelacion del SAT.(en el SDK,getSucceeded()esfalseygetDetails()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, y01sinreplacementUuid, la falla enReplacementUuid, como antes. - Certificados (
getTaxFileService().create()):fileType4(Pfx), que el API aceptaba sin documentarlo y que ningún flujo usa, responde 400 de validación con la falla enFileTypey 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()esfalseygetDetails()trae la falla con el campo); antes respondía 200 y guardaba el archivo. Los tipos admitidos son0y1(certificado y llave privada del CSD) y2y3(certificado y llave privada de la FIEL); otro valor sigue respondiendo'File Type' tiene un rango de valores que no incluye '<valor>'. - Elementos
nullen listas (facturasgetInvoiceService().create(), tambiénPOST /api/v4/invoices/calculate, que este SDK no usa; productosgetProductService().create()ygetProductService().update(); cancelacióngetInvoiceService().cancel(), por valores y por referencias): cada elemento de una lista de objetos del cuerpo debe ser un objeto. Un elementonull(por ejemplo, enitems,relatedInvoices, las percepciones de nómina,productTaxesotaxCredentials) responde 400 de validación conSorry, it's not me, it's you.yOne or more validation failures have occurred., una falla por cada elementonull(en el SDK,getSucceeded()esfalseygetDetails()trae las fallas con su campo), antes de las demás validaciones y sin llamar al PAC ni consumir timbre.propertyNamees la ruta del elemento con su posición (por ejemplo,RelatedInvoices[0],Items[1],Complement.Payroll.Earnings.Earnings[0],ProductTaxes[1]oTaxCredentials[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,partsycustomsInfode los conceptos se timbraban sin él; en la cancelación por referencias,taxCredentialsse ignoraba);taxCredentials,paidInvoices,paidInvoiceTaxesyubicaciones, que ya respondían 400 con otro texto (la falla del par de certificados yno 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 ymetadata(un diccionario) sigue aceptando valoresnull. El SDK no agrega elementosnullpor 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()esfalseygetDetails()trae la falla con el campo), antes de llamar al PAC y sin consumir timbre: engetInvoiceService().create()con el emisor por valores (taxCredentialsdel emisor), enIssuer.TaxCredentials[n].Password(n= posición de la llave, desde 0); engetTaxFileService().create()con una llave privada (fileType1o3) y engetManifestService().sign(), enPassword. El mismo mensaje sale si el archivo enviado como llave no es una llave privada. Antes respondía 400 sin falla de validación, conThe .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 engetManifestService().sign()), responden 400 conNo se pudo leer el certificado (.cer) o la llave privada (.key).(en el SDK,getHttpStatusCode()400 y ese texto engetDetails()) (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 engetTaxFileService().getDefaultValues()), responde 500 conSorry, it's not you, it's me.yNo pudimos descifrar un dato protegido del servidor. Escríbenos a soporte@fiscalapi.com.(en el SDK,getHttpStatusCode()500 y ese texto engetDetails()): es un error del servidor, no de la petición; escriba a soporte. Antes respondía 400 conThe .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 enPersonIdy el mensajeLa persona ya tiene datos de empleador: modifícalos con PUT /api/v4/people/{personId}/employer., ygetPersonService().getEmployeeService().create()para una persona que ya tiene datos de empleado, con la falla enEmployeePersonIdy el mensajeLa persona ya tiene datos de empleado: modifícalos con PUT /api/v4/people/{personId}/employee.(en el SDK,getSucceeded()esfalseygetDetails()trae la falla con el campo); modifíquelos congetPersonService().getEmployerService().update()ogetPersonService().getEmployeeService().update(). Antes respondía 400 sin falla de validación, conSorry, it's not you, it's me.yA record with the same unique values already exists. - Decimales de importes, cantidades y tasas (
getInvoiceService().create(); tambiénPOST /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 conSorry, it's not me, it's you.yOne or more validation failures have occurred., una falla por valor en su campo (propertyNamecon la ruta en PascalCase, por ejemploItems[0].Discount;attemptedValuees 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()esfalseygetDetails()trae las fallas con su campo), antes de llamar al PAC y sin consumir timbre. Por ejemplo,discount0.30000000000000004responde enItems[0].Discount'discount' admite como máximo 6 decimales (se recibieron 17): redondéalo antes de enviarlo; FiscalAPI no lo redondea.Límites: 6 enquantity,unitPriceydiscountde los conceptos (yquantityyunitPricede sus partes),taxRatede sus impuestos,exchangeRate, los importes y tasas del complemento de pagos (exchangeRate,amount,previousBalance,paymentAmount,remainingBalance,subTotal,baseytaxRate) ymarketPriceygrantPricedestockOptions; 10 enequivalence; 2 en los importes de nómina (percepciones, horas extra, otros pagos, jubilación, separación, deducciones, incapacidades, salarios del empleado yownResourceAmountdel empleador); 3 endaysPaidytimePercentage. Antes el valor llegaba al PAC, que respondía un error de esquema sin el campo (CFDI40999,CRP20999oCFDI40117), y en nóminatotalInstallmentsydailyAmountjunto contotalOneTimese ignoraban y la factura se timbraba (ahora también responden el 400). El API no redondea: conBigDecimalel caso típico esnew BigDecimal(double)(por ejemplonew BigDecimal(0.1)tiene 55 decimales) o una división; useBigDecimal.valueOf(double)y redondee antes de asignarlo, por ejemplovalor.setScale(6, RoundingMode.HALF_UP)(2 en los importes de nómina). Además,daysPaidcon uno o dos decimales o con.0(por ejemplo15.5o15.0), que el PAC rechazaba conCFDI40999, 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 ejemplo200.0000000) lo sigue rechazando el PAC conCFDI40999. 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,TotalDeduccionesyTotalOtrosPagos) 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 conCFDI40168(Descuentodel concepto con más decimales que suImporte). En Java pasaba si elBigDecimalno llevaba los ceros:new BigDecimal("3322.2")se envía como3322.2. El XML y la respuesta llevan ahora, por ejemplo,5000.00en lugar de5000.0o5000(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 congetPersonService().getEmployerService().create()yupdate()ogetPersonService().getEmployeeService().create()yupdate():curp,employerRegistrationyoriginEmployerTindel empleador, ycurp,socialSecurityNumber,seniority,employeeNumber,department,positionybankAccountdel empleado se omiten del XML; antes el PAC respondíaCFDI40999. Si el tipo de contrato (01a08) exige el registro patronal, ahora responde el error fiscal del PAC (NOM42). (3) Factura con descuento: un concepto cuyodiscounttiene más decimales que su importe (por ejemplounitPrice200condiscount10.55) se timbra: el XML escribe elImportecon los decimales delDescuento(200.00), hasta 6; antes el PAC respondíaCFDI40168. (4) Nómina con una percepción045(acciones o títulos) constockOptions(marketPriceygrantPrice): antes respondía 500Something went wrong on our end.(en el SDK,getHttpStatusCode()500) sin llamar al PAC ni consumir timbre; ahora se timbra con el nodoAccionesOTitulos(ValorMercadoyPrecioAlOtorgarse) y la factura se guarda con él. - Errores del servidor (todos los métodos): un error inesperado del API (500) deja de traer en
detailsel 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 mismotraceIdentifier: respondeSomething went wrong on our end.conOcurrió 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 engetDetails()y el identificador engetTraceIdentifier()). El status ymessageno 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).messageindica además de qué lado está la causa: todo 5xx traeSorry, it's not you, it's me., salvo el 500 inesperado (Something went wrong on our end.), y todo 4xxSorry, it's not me, it's you.. Antes, los 5xx propios del API con un texto endetailstraíanSorry, it's not me, it's you.; por ejemplo, el 503 degetInvoiceService().send()cuando el proveedor de correo no está disponible y el 502 degetDownloadRequestService().create()cuando el SAT no pudo crear la solicitud (en el SDK,getMessage()pasa aSorry, it's not you, it's me.;detailsy el status no cambian). Conservan su propiomessageel 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 conSorry, it's not you, it's me.yLa 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 conThe 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 conSorry, it's not me, it's you.yOtra petición modificó o eliminó el registro después de leerlo. Vuelve a consultarlo e inténtalo de nuevo.(antes 400 conAn 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 conSorry, it's not me, it's you.(antesSorry, it's not you, it's me.), sin falla de validación y con uno de estos textos endetails: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.oUno o más valores numéricos exceden el máximo permitido.(antes en inglés, comoA 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 engetDetails()). 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 conPerson.getManifestStatusId()enSigned; 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 conNo existe un usuario con RFC <rfc> en el tenant actual.
- README y
Examples.javasinsetCapitalRegime.