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.
Cada número puede traer owner, de quién es: holder (el deudor),
third_party (un tercero), reference (una referencia), guarantor (un
garante) o unknown. Sin owner el número queda unknown. Según la regla del
país, un número que no es del deudor puede quedar fuera de las llamadas o solo
para localizarlo, sin mencionar la deuda. En un número que ya conocíamos,
owner solo cambia si lo mandás: si no viene, se conserva el que tenía (lo pudo
marcar una llamada o un operador). La respuesta muestra owner y ownerSource
(import, call, operator o api).
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.
Consentimiento y baja de contacto
Podés cargar el consentimiento por canal y la baja de contacto en el mismo request. Los cinco campos son booleanos y opcionales:
En una actualización, un campo ausente no se toca: mandar sólo
complianceConsentToCall: false revoca las llamadas y deja los demás
consentimientos como estaban. GET /v1/debtors/{id} devuelve enseguida lo que
escribiste.
complianceOptOut: true registra la baja igual que un pedido del propio
deudor: queda una nota en la ficha, el evento en la línea de tiempo y se
cancelan los mensajes de WhatsApp pendientes. Si ese registro falla, el ítem
sale rejected con internal aunque el deudor ya exista; reintentá con el
mismo externalId y la baja se vuelve a intentar, salvo que la marca ya haya
quedado escrita: en ese caso el reintento no repite la nota ni la cancelación
de mensajes. complianceOptOut: false
quita la baja, sin los efectos de arriba.
Deudas dentro del deudor
Cada deuda de la listadebts se identifica por su propio externalId,
obligatorio, único dentro de la cartera (no sólo 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:Excepción: si el lote devuelve algún ítemrejectedconinternal(error transitorio, conviene reintentar), la respuesta no se guarda bajo la clave. Reintentar con la mismaIdempotency-Keyreprocesa el lote y los ítems que ya se habían creado salenupdated. 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 respuesta2xx: si tu primer intento con una clave dio un error4xx, ese intento no quedó guardado, así que la mismaIdempotency-Keysimplemente corre de nuevo, con el cuerpo original o con el corregido. El409 idempotency_conflictaparece sólo cuando ya hay una respuesta2xxguardada para esa clave y el cuerpo (o la cartera) del nuevo request es distinto.
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 una
2xx), así que corre de nuevo y puede volver a fallar igual. - La misma
Idempotency-Keycon un cuerpo corregido, después de un4xx, es un reintento válido. Como el error no se guardó, la clave no está asociada a ningún cuerpo. El409sólo aparece si antes tuviste una respuesta2xxcon esa clave y ahora mandás un cuerpo distinto: ahí 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 diez códigos son una lista
cerrada.
El texto de
Mensaje es literal, tal como lo devuelve la API, siempre en
inglés: es para una persona leyendo logs, no para tu código. Ramificá la
lógica de tu integración por reason.code, nunca por el texto del mensaje.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) cobra este endpoint 1 unidad cada 50 deudores del lote, con un mínimo de 1 unidad, en vez de la unidad fija por request del resto de la API. Un lote de 500 deudores cuesta 10 unidades.
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.