Skip to content

Latest commit

 

History

History
143 lines (111 loc) · 78.1 KB

CHANGELOG.md

File metadata and controls

143 lines (111 loc) · 78.1 KB

Changelog

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

[4.0.450] - 2026-10-14

Webhooks

  • webhooks.verify(headers, body, secret, tolerance=webhooks.DEFAULT_TOLERANCE) (módulo fiscalapi.webhooks, también accesible como from fiscalapi import webhooks) verifica una entrega de webhook de FiscalAPI y devuelve su WebhookEvent: valida la firma Standard Webhooks (HMAC-SHA256 sobre webhook-id.webhook-timestamp.cuerpo con el secreto whsec_…, comparada en tiempo constante con hmac.compare_digest) y que webhook-timestamp no difiera del reloj del receptor más que la tolerancia (timedelta), hacia atrás o hacia adelante (por omisión webhooks.DEFAULT_TOLERANCE, 5 minutos). Acepta cualquier firma v1 válida del header webhook-signature (varias, separadas por espacio) y el secreto con o sin el prefijo whsec_. secret acepta un secreto o una lista de secretos para la rotación (FiscalAPI firma con el nuevo y el anterior durante 24 h). headers es el request.headers de Flask, Django o FastAPI (o un dict); los nombres se comparan sin distinguir mayúsculas.
    • Opera sobre el cuerpo crudo tal como llegó, de preferencia en bytes (request.get_data(), request.body, await request.body()); un str se firma con su UTF-8. Un cuerpo reserializado no verifica. Ejemplo para Flask en el README (sección 12).
    • Una entrega que no se puede tratar como auténtica (falta un header, ninguna firma coincide, timestamp fuera de la tolerancia, un cuerpo o un header que no es texto UTF-8 válido, revisado antes de la firma, o un cuerpo firmado que no es un evento JSON) lanza WebhookVerificationException (exportada desde fiscalapi), con la causa original en __cause__; un secreto que no es base64 o está vacío, una lista de secretos vacía o una tolerancia negativa lanzan ValueError, y un argumento None o de otro tipo lanza TypeError (errores de configuración del receptor), que en una lista de secretos indican la posición del secreto y nunca su valor.
  • Modelos nuevos exportados desde fiscalapi (pydantic, como los demás): WebhookEvent, el mismo sobre para todo evento, con id (igual al header webhook-id, se repite en cada reintento), type, created_at (datetime en UTC, tzinfo=timezone.utc; llega como ISO 8601 con fecha, hora y zona, y un epoch, una fecha sin hora o una hora sin zona rechazan la entrega con WebhookVerificationException), person_id (None si el recurso no tiene persona) y data (WebhookEventData) con related_object (WebhookRelatedObject: id, type, url), status y previous_status. related_object.url 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 related_object.id.
  • type 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 related_object.type webhook y related_object.url /api/v4/webhooks/{id}, sin persona ni estados.
  • Cambio aditivo: nada existente cambia. Sin dependencias nuevas.

[4.0.439] - 2026-10-12

Enum CreditType

  • Tipo de crédito TICKET (3) para transferir y leer créditos de ticket: stamps.transfer_stamps() con credit_type=CreditType.TICKET los transfiere con las mismas reglas que los timbres, y las transacciones (stamps.get_list(), stamps.get_by_id()) los devuelven como CreditType.TICKET. El saldo de créditos de ticket se lee en balances de la persona (CreditBalance con credit_type igual a CreditType.TICKET); 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 desconocido ya no fallan: el valor se conserva. Antes, stamps.get_list() y stamps.get_by_id() lanzaban ValidationError si una transacción traía un creditType que el SDK no conocía (por ejemplo el 3 antes de este cambio) y se perdía la respuesta completa. Ahora StampTransaction.credit_type es Optional[Union[CreditType, int]]: un tipo conocido sigue llegando como miembro de CreditType y uno desconocido llega como su número (int). El código que compara contra CreditType (t.credit_type == CreditType.STAMP) no cambia; si su código usa atributos del enum (t.credit_type.name), compruebe antes isinstance(t.credit_type, CreditType).

Servicio de tickets (Auto CFDI)

  • Servicio de tickets (Auto CFDI): tickets.create (con Idempotency-Key opcional), get_list con filtros, get_by_id, get_code y cancel, en client.tickets (TicketService):
    • create(ticket, idempotency_key=None): POST /api/v4/tickets; cobra 1 crédito de ticket y devuelve TicketIngestionResult (id, ticket_code, qr_url, status, self_invoice_eligible, eligible_until y los importes calculados). Con idempotency_key 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 del API en details y su data en error_data.
    • get_list(filters=None): GET /api/v4/tickets con TicketListFilters: status (uno o más; se repite la llave status), person_id, details (con True envía details=true: cada ticket trae sus conceptos con impuestos, sin la historia; False no se envía), global_invoice_id (factura global que ampara los tickets), from_/to (date, fecha civil de México; from_ lleva guion bajo porque from es palabra reservada), page_number y page_size; los filtros en None no se envían.
    • get_by_id(ticket_id, details=False): con details=True trae items (con item_taxes) y events. get_code(ticket_id) devuelve TicketCodeResult (ticket_code, qr_url) para reimprimir. cancel(ticket_id) usa DELETE (solo desde pending; el crédito no se devuelve).
  • Modelos nuevos exportados desde fiscalapi: Ticket, TicketIssuer, TicketItem, TicketItemTax, TicketEvent, TicketIngestionResult, TicketCodeResult y TicketListFilters. payment_date es un datetime en hora civil de México (sin zona viaja como yyyy-MM-ddTHH:mm:ss); los decimales (Decimal) se envían tal cual.
  • Enums TicketStatus, TicketEventType y TicketActorType (str, Enum): status, events[].type y events[].actor_type son Union[<enum>, str]; un valor que el SDK no conoce llega como texto, sin ValidationError.
  • El SDK de Python no define interfaces que el integrador implemente: FiscalApiClient solo gana el atributo tickets; quien usa el cliente del SDK no cambia nada.
  • Ejemplo: examples/ejemplos-tickets.py.

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 Python es InvoiceItem.ticket_id (Optional[str], alias ticketId): en None no viaja en el JSON. Un 409 por tickets no disponibles trae unavailableTickets (ticketId, status) en error_data. Ejemplo: examples/ejemplos-factura-global-tickets.py.

ApiResponse

  • error_data (Optional[Any]): nuevo miembro (aditivo) con el data crudo de una respuesta de error (el dict o la lista tal cual): por ejemplo existingTicketId en el 409 de tickets.create (response.error_data["existingTicketId"]) o expectedTotal y computedTotal en su 422. Es None en las respuestas exitosas y en los errores sin data. data, message, details, http_status_code y trace_identifier no cambian (en un error, data sigue en None).

[4.0.433] - 2026-10-10

Cambios incompatibles (BREAKING)

  • Se elimina Person.capital_regime (estaba deprecado): el API no tiene régimen de capital, ignoraba el valor y nunca lo devolvía. Quite capital_regime=... de los Person(...) que construya y toda lectura de person.capital_regime (ahora lanza AttributeError); envíe la razón social sin régimen de capital en legal_name. Pasarlo al constructor no lanza error, pero se descarta sin enviarse.
  • TaxCredential deja de heredar de BaseDto: solo tiene base64_file, file_type y password (opcional en el .cer). Quite id=..., created_at=... y updated_at=... de los TaxCredential(...) que construya (se descartan sin enviarse) y toda lectura de esos atributos (ahora lanza AttributeError). Un código que trate un TaxCredential como BaseDto (por ejemplo isinstance(cred, BaseDto)) ya no lo reconoce.
  • 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 (el modelo ya los descartaba), 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.
  • Certificados: el API deja de devolver la contraseña de la llave privada (.key) en las consultas de certificados: tax_files.get_list(), tax_files.get_by_id() y la respuesta de tax_files.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 tax_files.get_default_values() la devuelve (a la persona misma o al owner del tenant); tax_files.get_default_references() ya la devolvía vacía. El modelo TaxFile no cambia (la descripción del campo lo documenta): la contraseña solo se envía al subir con tax_files.create(). Si su integración lee tax_file.password de una de esas consultas, use tax_files.get_default_values() 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 campos opcionales:
    • phone_number (phoneNumber): lo aceptan la creación y la actualización de personas.
    • balances: saldos por tipo de crédito (CreditBalance: credit_type y available, solo lectura). Solo aparecen los tipos que la persona ha tenido; un tipo ausente tiene saldo 0, y un tipo que el SDK no conoce conserva su número.
    • is_owner (isOwner): True si la persona es el owner de su tenant (solo lectura).
    • country: país de residencia fiscal expandido (solo lectura).
    • valid_to (validTo, datetime): fin de vigencia de la persona (solo lectura). La asigna el API; casi siempre es None y es informativa (no limita el timbrado ni el acceso al API).
  • password: requerida al crear; al actualizar, None o vacía conserva la contraseña actual. El API nunca la devuelve.
  • user_type_id: "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".

ApiResponse

  • trace_identifier (traceIdentifier): nuevo campo opcional (aditivo) con el identificador de la petición en los registros de Fiscalapi. El API lo envía en sus errores (por ejemplo, un 500 o un 502, cuyos textos piden enviarlo a soporte@fiscalapi.com) y el SDK lo copia ahora también en las respuestas fallidas, incluido el 400 de validación (antes lo descartaba); queda None si el cuerpo no lo trae. message, details y http_status_code no cambian. Igual que getTraceIdentifier() del SDK de Java.

TaxCredential

  • password deja de ser obligatorio en el modelo: el API solo lo exige en la llave privada (.key, file_type=1); en el certificado (.cer) no lo usa.

InvoicePayment

  • currency_code es str (antes Literal["MXN", "USD", "EUR"]): acepta cualquier clave del catálogo c_Moneda del SAT (por ejemplo CAD o JPY), que el API valida (400 en Complement.Payment.CurrencyCode si no está en el catálogo, viene en minúsculas o es XXX). Antes el modelo rechazaba localmente las monedas distintas de esas tres. El valor por omisión sigue siendo "MXN"; el código existente no cambia.

Claves de catálogo en los modelos

  • ProductTax.tax_id, ProductTax.tax_flag_id, ProductTax.tax_type_id, Person.sat_tax_regime_id, Person.sat_cfdi_use_id, Person.user_type_id y tax_flag_code de ItemTax, PaidInvoiceTax y LocalTax son str (antes Literal con las claves del catálogo): el SDK envía el valor tal cual, como los SDK de .NET, Node.js, PHP y Java, y el API lo valida (400 de validación con la falla en el campo; ver las notas de productos, personas y claves de catálogo del timbrado). Antes pydantic rechazaba al construir el modelo (ValidationError, sin llamar al API) cualquier otro valor, incluida la cadena vacía "", que el API acepta en las personas (la guarda sin valor). El código que ya usaba claves del catálogo no cambia.
  • TaxFile.file_type y TaxCredential.file_type son int (antes Literal[0, 1]): el SDK envía el valor tal cual, como los SDK de .NET, Node.js, PHP y Java, y el API lo valida (en tax_files.create(), 400 de validación en FileType si no es un tipo de archivo admitido; en TaxCredential, el timbrado y la cancelación por valores exigen el par CSD, 0 y 1, y si no responden 400 en TaxCredentials). Antes pydantic rechazaba localmente 2 y 3 (certificado y llave privada FIEL), que tax_files.create() acepta, y fallaba al leer la respuesta de tax_files.get_list() o tax_files.get_by_id() con un archivo FIEL (ValidationError). El código que ya usaba 0 o 1 no cambia. 4 (Pfx) lo rechaza tax_files.create() con 400 en FileType (ver la nota de certificados).
  • CancelInvoiceRequest.cancellation_reason_code es str (antes Literal["01", "02", "03", "04"]): el SDK envía el valor tal cual, como los SDK de .NET, Node.js, PHP y Java, y el API valida la clave contra el catálogo c_MotivoCancelacion antes del PAC (400 de validación en CancellationReasonCode; ver la nota de cancelación). Antes pydantic rechazaba localmente cualquier otro valor (ValidationError, sin llamar al API). El código que ya usaba 01 a 04 no cambia.

Modelo DownloadRequest

  • DownloadRequest.next_attempt_date (nextAttemptDate) queda obsoleto (lo indica su descripción) y se eliminará en una versión mayor. El API ya no devuelve nextAttemptDate en las solicitudes de descarga (download_requests.get_list(), download_requests.get_by_id(), download_requests.search() y download_requests.create()): siempre era null, porque Fiscalapi no programa un siguiente intento por solicitud (revisa las solicitudes pendientes periódicamente). El campo sigue existiendo y vale None, como antes: quite esa lectura (la fecha del último intento está en last_attempt_date).

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

  • Person.tax_password 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 tax_credentials). Solo la reciben con valor la propia persona y el owner del tenant; los demás reciben None. Al actualizar, None la conserva (el SDK no envía los None) y "" la borra.
  • ValidationFailure.attempted_value: 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.
  • El API ya no devuelve twoFactorEnabled en las personas (este SDK nunca lo modeló).
  • TaxFile.tin es opcional en tax_files.create(): si se omite (None 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). valid_to y committed_balance son de solo lectura: el API los ignora al crear o actualizar. committed_balance es un campo heredado que el API ya no calcula y siempre vale 0; para el saldo use available_balance, available_validation_balance o balances.
  • Personas: people.create() y people.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, succeeded es False y details 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: people.update() y people.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, http_status_code 409 y ese texto en details) 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: people.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 api_keys.get_list() ni en api_keys.get_by_id() (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: people.create() y people.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, succeeded es False y details 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, details 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í people.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 (invoices.create()): con una moneda extranjera (currency_code distinto de MXN y de XXX), exchange_rate 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, succeeded es False y details 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.exchange_rate frente a complement.payment.currency_code): con MXN, exactamente 1 (si es None, 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 (invoices.create()): con currency_code XXX (facturas de pago y de traslado), un exchange_rate 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, succeeded es False y details 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 None, el API usa 1) o 0 se sigue timbrando, sin TipoCambio en el XML.
  • Facturas de pago (invoices.create() con complement.payment): la moneda del pago (complement.payment.currency_code) y la de cada factura pagada (currency_code de cada elemento de complement.payment.paid_invoices) 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, succeeded es False y details trae la falla con el campo); antes llegaban al PAC, que las rechazaba (CFDI40113, CRP20213 o CRP20236). La moneda de la factura de pago (currency_code) 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). Una factura sin currency_code responde una sola falla en CurrencyCode (antes, dos).
  • Factura global (invoices.create() con global_information.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, succeeded es False y details 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: tax_files.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 tax_files.create().
  • Certificados: el servicio de certificados no tiene update (tax_files nunca lo tuvo en este SDK, así que su forma no cambia): 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 tax_files.create() y elimine el anterior con tax_files.delete().
  • Facturas (invoices.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, succeeded es False y details 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 invoices.create() y invoices.get_by_id() trae Invoice.global_information: completa (periodicity_code, month_code y year) en una factura global, o None 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 None).
  • Facturas (invoices.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, succeeded es False y details 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 None) 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 (invoices.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 (invoices.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, succeeded es False y details 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 (invoices.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, succeeded es False y details 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 (invoices.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, succeeded es False y details 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 (complement.comercio_exterior.receptor.num_reg_id_trib) se ignora, como antes: el API usa el del receptor del CFDI (recipient.foreign_tin, 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 message era Error processing server error response y details el cuerpo crudo; ahora succeeded es False, message es Sorry, it's not me, it's you. y details trae las fallas como Campo: mensaje separadas por ; . En esas fallas errorCode es null. Un cuerpo que no se puede leer responde una sola falla: 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 details; 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 (property_name) en su lugar.
  • Certificados, timbrado y cancelación por referencias: tax_files.get_default_references(), tax_files.get_default_values(), invoices.create() con el emisor por referencia (issuer.id) y invoices.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, http_status_code 404 y ese texto en details); suba el archivo que falta. Antes, las referencias por defecto y la cancelación respondían un error 500, get_default_values() 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. get_default_values() 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: tax_files.get_default_references() aplica la misma regla que tax_files.get_default_values(): 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, http_status_code 403 y ese texto en details); 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, http_status_code 403 y ese texto en details); antes el API permitía modificarla o borrarla. DELETE de un id que no existe responde 404 (antes, 400 con false).
  • Facturas por referencias (invoices.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, http_status_code 400 y ese texto en details). 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 (invoices.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, http_status_code 404 y ese texto en details); 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 invoice_uuid, tin y tax_credentials. 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 (download_rules.create(), download_rules.update() y download_rules.delete()): solo el owner del tenant crea, actualiza o elimina una regla de la persona owner (person_id 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, http_status_code 403 y ese texto en details) 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: download_requests.get_xmls() (GET /api/v4/download-requests/{id}/xmls, Xml.date) y download_requests.get_metadata_items() (GET /api/v4/download-requests/{id}/meta-items, MetadataItem.invoice_date, MetadataItem.sat_certification_date y MetadataItem.cancellation_date) 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.time_zone); 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.date = datetime(2025, 6, 18, 12, 58, 30)), ahora "date": "2025-06-18T18:58:30.000" (datetime(2025, 6, 18, 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 (created_at, updated_at, start_date, end_date y last_attempt_date) no cambian.
  • Consultas por id: con un id que no existe o que es de otra cuenta, invoices.get_by_id(), invoices.get_xml(), invoices.get_pdf() (invoice_id), invoices.get_status() (id), people.get_by_id(), people.update(), people.delete(), stamps.get_by_id(), products.get_by_id(), download_rules.get_by_id(), download_rules.delete(), download_requests.get_by_id(), download_requests.delete(), download_requests.download_package(), download_requests.download_sat_request() y download_requests.download_sat_response() responden 404 con No such resource for the given Id: <id> (en el SDK, http_status_code 404 y ese texto en details), la misma respuesta que ya daban otras rutas por id (por ejemplo invoices.send()); el message es Sorry, it's not me, it's you. Antes cada una tenía su propio texto: invoices.get_by_id(), Not found such resource: <id>; invoices.get_xml(), No such resource for the given id: <id>; invoices.get_status(), stamps.get_by_id(), download_rules.delete(), download_requests.delete(), download_requests.download_sat_request() y download_requests.download_sat_response(), No such resource for given id: <id>; people.get_by_id() y people.delete(), No such resource for the given id:<id>; products.get_by_id(), Product with id: <id> not found; download_rules.get_by_id(), Rule with id: <id> not found; download_requests.get_by_id(), Sat request with id: <id> not found; y download_requests.download_package(), No existe una solicitud de descarga con id <id>. con el message Not Found. invoices.get_pdf() y people.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 people.update(). Si su integración reconoce el "no encontrado" por el texto, use el código 404.
  • Catálogos (catalogs.get_record_by_id()): una clave que no existe en el catálogo responde 404 con No such resource for the given Id: <id> (en el SDK, http_status_code 404 y ese texto en details); 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; products.create() y products.update(), people.employee.create() y people.employee.update(), people.employer.create() y people.employer.update()): unit_price 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 product_taxes (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). base_salary_for_contributions e integrated_daily_salary del empleado, y own_resource_amount del empleador (si se envía, con None no se valida), deben ser mayores o iguales a 0 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 own_resource_amount mayor, respondía 500).
  • Productos (products.create() y products.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, succeeded es False y details 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, products.update() con un impuesto inválido respondía el error y dejaba el producto sin impuestos). En este SDK, tax_id, tax_flag_id y tax_type_id de ProductTax son str (ver «Claves de catálogo en los modelos»): el SDK envía el valor tal cual y estas fallas las responde el API.
  • Personas (people.create() y people.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, succeeded es False y details 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). En este SDK, sat_tax_regime_id, sat_cfdi_use_id, user_type_id y country_id de Person son str (ver «Claves de catálogo en los modelos»): el SDK envía el valor tal cual (también ""; para no enviar valor use None) y estas fallas las responde el API.
  • Facturas (invoices.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, succeeded es False y details 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 (catalogs.get_record_by_id(), catalogs.search_catalog() y download_catalogs.list_catalog()): un nombre de catálogo que no existe responde 404 con No existe el catálogo '<nombre>'. (en el SDK, http_status_code 404 y ese texto en details); 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 (products.create() y products.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, succeeded es False y details 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 (invoices.create()) y de los datos de empleado y empleador (people.employee.create() y people.employee.update(); people.employer.create() y people.employer.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, succeeded es False y details 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 (invoices.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, succeeded es False y details 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 (invoices.create()): items[n].unitOfMeasurement (este SDK no lo modela en InvoiceItem) 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 unit_of_measurement_code (catálogo c_ClaveUnidad).
  • Mensajes del timbrado en español (invoices.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, succeeded es False y details 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 (invoices.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 (invoices.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, succeeded es False y details 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 (tax_files.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, succeeded es False y details 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 invoices.create(), también POST /api/v4/invoices/calculate, que este SDK no usa; productos products.create() y products.update(); cancelación invoices.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, succeeded es False y details 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. Al construir un modelo, pydantic ya rechaza un None dentro de una lista de modelos (ValidationError, sin llamar al API); si se agrega después (por ejemplo, con append), el SDK lo envía como null y el API responde este 400.
  • 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, succeeded es False y details trae la falla con el campo), antes de llamar al PAC y sin consumir timbre: en invoices.create() con el emisor por valores (taxCredentials del emisor), en Issuer.TaxCredentials[n].Password (n = posición de la llave, desde 0); en tax_files.create() con una llave privada (fileType 1 o 3) y en manifests.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 (invoices.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 manifests.sign()), responden 400 con No se pudo leer el certificado (.cer) o la llave privada (.key). (en el SDK, http_status_code 400 y ese texto en details) (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 tax_files.get_default_values()), 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, http_status_code 500 y ese texto en details): 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: people.employer.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 people.employee.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, succeeded es False y details trae la falla con el campo); modifíquelos con people.employer.update() o people.employee.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 (invoices.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, succeeded es False y details 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: el caso típico es un Decimal construido desde un float (por ejemplo 0.1 + 0.2, que llega como 0.30000000000000004) o el resultado de una división; redondéelo antes de asignarlo, por ejemplo valor.quantize(Decimal('0.000001')) (Decimal('0.01') 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 (invoices.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). Con este SDK pasaba con un Decimal construido desde un float, que no lleva ceros finales (3322.20 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 people.employer.create() y people.employer.update() o people.employee.create() y people.employee.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, http_status_code 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, http_status_code 500, ese texto en details y el identificador en trace_identifier). 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 invoices.send() cuando el proveedor de correo no está disponible y el 502 de download_requests.create() cuando el SAT no pudo crear la solicitud (en el SDK, message 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, http_status_code 503, 409 o 400 y el texto en details). Cualquier otra falla de la base de datos es el 500 inesperado.
  • Carta manifiesto (manifests.sign()): todas las personas del tenant con el RFC del certificado quedan con manifest_status_id 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.