Escribir deudores por la API requiere una clave con el scope
write (ver
Autenticación). Sin ese scope,
POST /v1/portfolios/:id/debtors responde 403 forbidden. Si la cartera
del path no pertenece a la organización de la clave, o la organización no
tiene ninguna cartera activa, la respuesta es 404 not_found: la API
pública nunca distingue “no existe” de “no es tuya”. Un cambio de scope
reciente en la clave puede tardar hasta 60 segundos en hacerse efectivo,
por la caché descrita en Autenticación.Cuándo usar este endpoint y cuándo la carga de cartera
POST /v1/portfolios/:id/debtors está pensado para sincronizar deudores
desde tu propio core: lotes de hasta 500 ítems, con un resultado por ítem en
la misma respuesta. Para el alta inicial de una cartera de miles de deudores,
usá la carga de cartera por CSV: es
asíncrona y no tiene ese tope.
Crear o actualizar deudores
results.length siempre es igual a debtors.length, en el mismo orden,
pero sólo si el request pasó la validación de esquema. Antes de procesar
un solo ítem, el esquema del request completo (minItems: 1 de debtors,
los maxItems de debts (20), contactInfoPhoneNumbers (10) y
contactInfoEmails (5), totalAmount mayor a 0, currency con formato
^[A-Z]{3}$, contactInfoAddressCountry con formato ^[A-Z]{2}$, los
patrones de fecha, el formato de email, y cada maxLength) rechaza el
lote entero con 400 validation_error y un array issues[] con el
detalle por campo: no llega a correr ni un ítem. La tabla de “Rechazos por
ítem”, más abajo, describe rechazos que sólo pasan sobre un request que ya
cruzó esa validación de esquema; dentro de ese conjunto, sí es cierto que un
ítem rechazado no afecta a los demás.
Cómo se identifica a un deudor
Cada deudor se identifica por suexternalId dentro de la cartera: es la
clave que vos elegís del lado de tu sistema. Un reintento con el mismo
externalId en la misma cartera actualiza en vez de duplicar. No hace falta
que sepas si Mozart ya lo creó.
Si el número de documento de un deudor cambia de tu lado, mandá el mismo
externalId con el documento nuevo: la API pisa identityDocumentType e
identityDocumentNumber porque son SIEMPRE (ver la tabla de abajo).
Qué pisa una actualización y qué no
La regla general: lo que mandás pisa, lo que no mandás se conserva. Los teléfonos son la excepción: se fusionan conmergeImportedPhoneNumbers, sin
perder lo que Mozart aprendió operando (si un número ya está verificado o
tiene un tipo asignado, esa marca se conserva mientras el número siga
apareciendo en la lista que mandás).
Detalle de teléfonos: la lista que mandás decide qué números existen y en
qué orden. Un número que no incluyas en la lista deja de estar asociado al
deudor. Un número que ya conocíamos (mismo número, sin importar el formato
exacto en que lo mandes) conserva lo que Mozart aprendió operando con él
(si está verificado, su tipo, su origen) y sólo actualiza el string y si es
primary. Si no mandás contactInfoPhoneNumbers, o lo mandás vacío, los
teléfonos existentes no se tocan.
contactInfoEmails, cuando viene, reemplaza la lista entera: a
diferencia de los teléfonos, no se fusiona, así que se pierden las marcas de
verificación de los correos anteriores.
Deudas dentro del deudor
Cada deuda de la listadebts se identifica por su propio externalId,
obligatorio, único dentro del ítem del deudor. El match es por
(cartera, externalId):
- Si el
externalIdde una deuda ya existe viva bajo este mismo deudor, se actualiza en el lugar. - Si no matchea ninguna deuda existente, se crea.
- Si ya existe viva bajo otro deudor de la cartera, el ítem entero del
deudor sale rechazado con
debt_external_id_in_use: no se crea nada de ese ítem.
debts no se
toca: esta operación no reconcilia. Este endpoint nunca borra deudas.
Cada deuda que mandás es una foto completa, igual que una fila del
importador: totalAmount es obligatorio y tiene que ser mayor a 0,
paidAmount que se omite vale 0, y currency es obligatoria. dueDate,
issueDate, description y debtType pisan si los mandás y se conservan
si no. La currency de una deuda existente nunca cambia por esta vía: si
mandás una moneda distinta de la guardada, el ítem sale rechazado con
debt_currency_mismatch; para eso necesitás una deuda nueva con otro
externalId.
El status de la deuda se deriva de los montos, nunca se manda directo: pasa
a paid cuando paidAmount queda igual a totalAmount, y una deuda que
estaba paid vuelve a active si un envío posterior la deja con
paidAmount menor a totalAmount. Una deuda cancelled no cambia de estado
por esta vía.
Idempotencia
Hay dos niveles independientes:-
Por
externalId(idempotencia de negocio, siempre activa): reintentar el lote entero sin ninguna cabecera especial daupdateden vez decreatedpara los deudores y deudas que ya existían: nada se duplica. -
Por
Idempotency-Key(idempotencia de transporte, opcional): mandá la cabeceraIdempotency-Keycon un valor propio, hasta 255 caracteres. Si no la mandás, o la mandás vacía, el request corre siempre y no queda ningún registro de idempotencia de transporte: no es un error, sólo no tenés protección contra un reintento de red. Si la mandás con más de 255 caracteres, la respuesta es400 validation_error, antes de tocar la base. Reenviar la misma request con la misma clave (dentro de la vigencia) devuelve exactamente la misma respuesta, sin volver a procesar nada, con la cabeceraIdempotent-Replayed: true:La clave queda vigente 24 horas y está acotada a tu propia API key: la mismaIdempotency-Keyusada por otra clave no colisiona con la tuya. Sólo se guarda una respuesta200: si tu primer intento con una clave dio un error, ese intento no quedó guardado, así que podés reintentar la mismaIdempotency-Keycon el cuerpo corregido sin chocar contra un409. Si en cambio reusás la misma clave con el cuerpo original sin corregir (o contra otra cartera), la respuesta es409 idempotency_conflict.
Reintentos
- Timeouts y errores 5xx son seguros para reintentar a ciegas, incluso
sin
Idempotency-Key: elexternalIdde cada deudor y el de cada deuda hacen que un reintento sea unupdate, no una duplicación. - Un error 4xx no es seguro para reintentar tal cual: corregí el cuerpo
antes de reenviar. Reintentar el cuerpo sin corregir no arregla nada
porque esa respuesta nunca se guardó (sólo se guarda un
200), así que corre de nuevo y puede volver a fallar igual. - La misma
Idempotency-Keycon un cuerpo corregido es un409, no un reintento válido. La clave ya está asociada al hash del cuerpo original. Para el cuerpo corregido, usá unaIdempotency-Keynueva (o ninguna). - Un lote reenviado pisa montos, no los acumula. Cada deuda que mandás
es una foto completa: si reenviás un lote con un
paidAmountviejo, sobreescribís el valor más reciente que Mozart tenía guardado. No trates un reenvío de un lote pasado como inocuo.
Rechazos por ítem
Un ítem rechazado no afecta a los demás:results siempre tiene la misma
longitud y el mismo orden que debtors, así que podés recorrer ambos arrays
en paralelo. Esto describe los rechazos por ítem sobre un lote que ya pasó
la validación de esquema (ver más arriba): los ocho códigos son una lista
cerrada.
Límites
- 500 deudores por lote. Un request con más ítems responde
413 payload_too_large, con un mensaje que te deriva a la carga de cartera (POST /v1/portfolios/:id/imports/upload-url+POST /v1/portfolios/:id/imports), que corre asíncrona y sin ese tope. - 5 MB por cuerpo de request. Un lote que supere ese tamaño, aunque
tenga menos de 500 ítems, responde el mismo
413 payload_too_largede arriba, con el mismo tipo de mensaje. Este chequeo corre antes que el de cantidad de ítems, así que un lote de pocos deudores con campos muy largos también puede caer acá. - Hasta 20 deudas, 10 teléfonos y 5 emails por deudor.
- El límite de uso por minuto (ver Paginación, errores y límites) cuenta este request como uno solo, sin importar cuántos deudores lleve el lote.
Teléfonos
Cadaphone tiene que cumplir el formato E.164: un +, seguido del código
de país (el primer dígito no puede ser 0) y el número, con el total del
número entre 9 y 15 dígitos.
Ejemplos de error
Los dos errores propios de este endpoint que no son un400
validation_error genérico:
413: lote demasiado grande
409: Idempotency-Key reusada con otro cuerpo
unauthorized, forbidden, not_found, rate_limited), está en
Paginación, errores y límites.