Vas a lograr crear o actualizar hasta 500 deudores (con sus deudas) en una sola llamada desde tu propio sistema, sin duplicarlos en un reintento, y entender qué pisa cada campo y qué se conserva.
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 su externalId 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).
Un deudor borrado no revive. Si un externalId que ya usaste corresponde a un deudor que Mozart borró (baja lógica), el próximo envío con ese mismo externalId crea un deudor nuevo, con otro id interno: no reactiva al anterior.

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 con mergeImportedPhoneNumbers, 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.
contactInfoEmails: [] (lista vacía) borra todos los emails guardados: para este campo, “vacío” es una instrucción, no un “no lo toques”. Es el comportamiento opuesto al de contactInfoPhoneNumbers: [], que no modifica los teléfonos existentes (ver el párrafo de arriba). La misma lista vacía significa cosas distintas en los dos campos.

Deudas dentro del deudor

Cada deuda de la lista debts se identifica por su propio externalId, obligatorio, único dentro del ítem del deudor. El match es por (cartera, externalId):
  • Si el externalId de 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.
Una deuda que el deudor ya tiene guardada y que no viene en 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:
  1. Por externalId (idempotencia de negocio, siempre activa): reintentar el lote entero sin ninguna cabecera especial da updated en vez de created para los deudores y deudas que ya existían: nada se duplica.
  2. Por Idempotency-Key (idempotencia de transporte, opcional): mandá la cabecera Idempotency-Key con 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 es 400 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 cabecera Idempotent-Replayed: true:
    La clave queda vigente 24 horas y está acotada a tu propia API key: la misma Idempotency-Key usada por otra clave no colisiona con la tuya. Sólo se guarda una respuesta 200: si tu primer intento con una clave dio un error, ese intento no quedó guardado, así que podés reintentar la misma Idempotency-Key con el cuerpo corregido sin chocar contra un 409. Si en cambio reusás la misma clave con el cuerpo original sin corregir (o contra otra cartera), la respuesta es 409 idempotency_conflict.

Reintentos

  • Timeouts y errores 5xx son seguros para reintentar a ciegas, incluso sin Idempotency-Key: el externalId de cada deudor y el de cada deuda hacen que un reintento sea un update, 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-Key con un cuerpo corregido es un 409, no un reintento válido. La clave ya está asociada al hash del cuerpo original. Para el cuerpo corregido, usá una Idempotency-Key nueva (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 paidAmount viejo, 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_large de 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.
Los 5 MB de arriba son nuestro tope, no el único. El límite general de Lambda para cualquier request de la API es 6 MB, y lo aplica la plataforma antes de que nuestro código corra: un cuerpo de más de 6 MB también responde 413, pero con un sobre que NO es el nuestro: {"Message":"Request must be smaller than 6291456 bytes for the InvokeFunction operation"}, sin code ni requestId. Tratá cualquier 413 de este endpoint, sea nuestro sobre o el de la plataforma, como la misma señal: partí el lote o pasate a la carga de cartera.

Teléfonos

Cada phone 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 un 400 validation_error genérico:
413: lote demasiado grande
409: Idempotency-Key reusada con otro cuerpo
El formato completo del sobre de error, y el resto de los códigos (unauthorized, forbidden, not_found, rate_limited), está en Paginación, errores y límites.

Siguiente paso

Seguí con Carga de cartera si necesitás subir volúmenes grandes, o con Paginación, errores y límites para el formato completo del sobre de error. El detalle campo por campo del request y la respuesta está en la pestaña Referencia.