Appearance
Importar/migrar un crédito existente
POST
/import/receivable
Importa (migra) hacia Kuenta un crédito que ya existe en un sistema externo, junto con
el deudor y su historial de pagos. Está pensado para migraciones masivas de cartera:
el endpoint resuelve o crea la entidad del deudor, copia su perfil, crea el crédito y
ejecuta la amortización aplicando los pagos históricos que se envíen.
A diferencia de POST /receivables, aquí no se dispara análisis de crédito: el crédito
entra directamente con el status que indique el llamante y con la fecha de originación
(startedAt) del sistema de origen, de modo que la edad de cartera y la mora se calculen
como en el sistema legado.
El crédito importado queda marcado con source = "import-cli".
⚠️ Advertencias que debe leer antes de integrar
1. rate es un PORCENTAJE (EA), no una fracción
El valor recibido se divide entre 100 internamente
(parseRate en receivable_service.go). Es decir:
| Envía | Kuenta almacena | Significa |
|---|---|---|
"25.0452604625315" |
0.250452604625315 |
25,045 % EA ≈ 1,88 % mensual |
"0.25" |
(rechazado) | Se interpretaría como 0,25 % EA; el guard de escala lo rechaza con 412 |
Enviar "0.25" con la intención de "25 %" ya NO produce un crédito sin intereses:
un guard de escala rechaza con 412 ValidationFailed toda tasa resultante fuera del
rango [0,5 % ; 200 %] EA, precisamente para atajar este error de unidad. Ojo con el
borde: "0.5" (queriendo 50 %) cae exactamente en el mínimo y sí se acepta como
0,5 % EA; toda tasa por debajo de 5 % EA queda trazada en el log del servidor para
poder auditar la migración después.
"0" sí se acepta: un crédito sin intereses es legítimo. La única excepción es una
línea de crédito con cambios de tasa habilitados, donde se rechaza con
412 ValidationFailed: el motor de amortización interpreta un techo de tasa en cero
como "sin definir" y caería al techo de la línea, así que un 0 % ahí no se puede
representar sin que mute solo.
Verifique siempre la escala contra la cuota esperada antes de importar en lote.
Además, parseRate elimina el signo % y convierte la coma decimal en punto. Por eso
un separador de miles se malinterpreta en silencio: "1,234" se lee como 1.234
(⇒ 0,01234 EA), no como 1234. Nunca use separador de miles.
2. La tasa de mora (debtRate) por defecto sale de la línea de crédito
debtRate es opcional y se expresa en la misma unidad que rate (porcentaje EA
como cadena). Se resuelve con el mismo modelo que la originación normal: el valor del
payload es un override y, si no viene, manda la tasa de mora configurada en la línea de
crédito. rate y debtRate son independientes: ninguno se deriva del otro.
Sólo si la línea no tiene mora configurada (cero) se cae a la tasa corriente del propio
crédito, para no dejar el crédito con una mora en cero (que el motor de amortización
interpreta como "sin definir").
Sobre el default no se aplica el tope de usura: la importación reconstruye historia,
y recortar un crédito antiguo contra la usura vigente hoy alteraría el dato migrado.
Si la cartera de origen maneja una mora por crédito distinta de la de la línea,
envíela explícitamente.
3. Las tasas quedan fijadas en el crédito, no se recalculan contra la línea
Además de Rate y DebtRate, el crédito importado guarda sus topes
(CurrentRate, MaxRate, CurrentDebtRate, MaxDebtRate) con la tasa importada.
Esto importa porque el motor de amortización trata un tope en cero como
"sin definir" y, en ese caso, recalcula contra la tasa configurada en la línea de
crédito. Al fijarlos explícitamente, la tasa que se ve en la ficha del crédito, en los
reportes y en los exportes es la que se importó, y no la de la línea.
Es la misma razón por la que un rate de 0 sólo se rechaza sobre líneas con cambios
de tasa habilitados (ver advertencia 1): ahí el cero sería ambiguo.
4. Idempotencia por externalID
externalID es la llave de idempotencia. Si ya existe un crédito importado con ese
externalID para la organización autenticada, no se crea un duplicado: se reutiliza
el crédito existente. Reintentar la misma petición es seguro.
La aplicación de payments también es idempotente: cada pago recibe un identificador
determinístico derivado de (organización acreedora, externalID, paymentDate,
paymentValue, ordinal dentro de ese grupo de fecha+monto), así que reintentar no
vuelve a abonar los mismos pagos. El ordinal es lo que permite que dos pagos legítimos
del mismo día por el mismo valor se apliquen ambos en vez de confundirse con un
duplicado. Ese identificador es compartido con la importación de pagos por CSV, de modo
que un pago cargado por ambas vías se deduplica.
5. Orden de los pagos
El orden del arreglo no importa: los payments se ordenan internamente por
paymentDate ascendente antes de aplicarse, porque el orden de aplicación afecta el
reparto entre capital, interés y mora. Enviarlos desordenados produce el mismo resultado
que enviarlos ordenados.
6. profile es la única forma de enviar la identidad del deudor
Los campos planos name, lastName y email en la raíz del payload
(contrato anterior a 2026-01) ya no se aceptan: la petición se rechaza con
412 ValidationFailed indicando que use profile.firstName, profile.lastName y
profile.email. Vea la sección Campos rechazados o ignorados más abajo.
7. Costos externos opcionales (costs)
costs usa exactamente el mismo formato que POST /payment-links/external.
Si se envía, reemplaza por completo los costos de la línea para este crédito y queda
guardado en el crédito: pagos, mora, activación y cambios de condiciones posteriores
siguen usando esos costos. Si se omite o va vacío, se usan los costos de la línea, como
hasta ahora. Un costo inválido rechaza toda la importación con 412 ValidationFailed
y no se crea nada.
8. creditLineID siempre resuelve a la versión vigente de la línea
Al editar una línea se crea una versión nueva y la anterior queda archivada. Si se envía
el ID de cualquier versión (la original, una intermedia o la vigente), el crédito se crea
sobre la versión vigente, así que no hace falta actualizar el payload después de cada
edición. Las versiones creadas por contraofertas nunca se eligen.
Resolución del deudor
phone es la llave con la que se resuelve el deudor. Se normaliza internamente a
formato E.164 con + y se busca una entidad natural con ese teléfono:
- El teléfono ya existe ⇒ se reutiliza esa entidad.
profilees opcional. - El teléfono no existe, pero el documento sí ⇒ se reutiliza la entidad hallada por
(documentType, document). No se crea un deudor duplicado yprofilesigue
siendo opcional. La relación con esta organización se crea si no existía. - No resuelve ni por teléfono ni por documento ⇒ se registra un deudor nuevo.
Sólo en este casoprofilees obligatorio, y lo único exigido es
profile.firstNameyprofile.lastName, con mínimo 2 caracteres cada uno;
de lo contrario se responde412 ValidationFailed.
profile.email, profile.idNumber y profile.idType no son obligatorios, aunque
enviarlos es muy recomendable: sin email el usuario queda sin correo de contacto, y
sin documento el deudor no se puede reutilizar por documento en importaciones futuras.
El documento y su tipo se resuelven campo por campo: cada uno se toma de la raíz
(document / documentType) y, si la raíz no lo trae, de profile.idNumber /
profile.idType. Mandar el número en la raíz y el tipo dentro de profile es válido. Si el documento resuelto no trae un tipo
válido se responde 412 ValidationFailed. Al crear una entidad nueva —o al completar
una existente que no tenía documento— se escribe sobre la entidad; si la entidad ya
tiene un documento distinto, no se sobrescribe.
Campos rechazados o ignorados
| Campo | Comportamiento |
|---|---|
name, lastName, email (raíz) |
Rechazados con 412 ValidationFailed. Eran el contrato anterior a 2026-01. Use profile.firstName, profile.lastName, profile.email. |
entityID |
Ignorado. Siempre se sobrescribe con la organización de la sesión autenticada (data.EntityID = session.Role.EntityID). Enviarlo no tiene efecto. |
frequency |
Se guarda tal cual viene, pero puede no ser la que se aplica. El valor enviado queda en el campo paymentFrequency del crédito; sin embargo, la amortización usa la frecuencia de la línea cuando ésta tiene exactamente una configurada, y el plazo total cuando la línea es de período único. Es obligatorio y debe ser mayor que cero: alimenta un divisor en el cálculo de cuotas. |
status |
Los estados previos al desembolso (0, 1, 2, 5, 13, 14) se respetan. Si se omite o se envía cualquier otro, el motor de amortización (setGlobalStatus) lo recalcula — por ejemplo, un crédito con cuotas vencidas termina en 10 (MORA). |
Hay tres formas de importar, y comparten las reglas
| Vía | Cómo se usa | Formato de fecha |
|---|---|---|
| Este endpoint REST | Un crédito por petición, con sus pagos en el mismo payload | YYYY-MM-DD |
| Carga masiva por CSV | Procesos por lotes "Importar Créditos" e "Importar Pagos" | YYYY-MM-DD |
CLI (import-credits / import-payments) |
Operación interna | dd/mm/aaaa |
Las reglas descritas arriba —contrato de identidad, resolución del deudor, escala de
tasas, default de mora desde la línea, startedAt estricto y topes de tasa— rigen en
las tres. Dos consecuencias prácticas:
- La idempotencia de pagos cruza las tres vías. El identificador determinístico se
calcula sobre la fecha ya normalizada, así que un pago cargado por CSV y re-enviado en
un payload REST (o al revés) no se abona dos veces, pese a que los formatos de
fecha de entrada sean distintos. - En el CSV, las columnas planas sí construyen la identidad (
Nombre,Apellido,
Correo,Documento), y la columnaProfilecon un JSON anidado las sobreescribe
campo por campo. Es la única diferencia deliberada con este endpoint: en una hoja de
cálculo las columnas son el canal natural, mientras que en un JSON el objeto anidado
ya existe y tener dos canales compitiendo fue justamente el origen del problema.
Limitación conocida del CSV de pagos: cada fila se procesa aislada, así que dos filas
con la misma fecha y el mismo monto para el mismo crédito se deduplican entre sí. Si la
cartera trae pagos legítimos repetidos el mismo día, impórtelos por este endpoint, que sí
los distingue.
Permisos requeridos
write:receivables
Encabezados obligatorios
Config-Organization-IDOrganization-ID
Authorizations
test
OAuth 2.0 Client Credentials Grant (RFC 6749 §4.4).
Utilice este flow para autenticación de aplicación-a-aplicación sin intervención del usuario.
Ideal para servicios backend y integración entre sistemas.
clientCredentials Flow
Token URL
https://test-api.kuenta.co/v1/oauth/tokenScopes:
*Acceso completo (administrador)users:readLectura de usuariosusers:writeEscritura de usuariosusers.info:writeEscritura de información de usuariosclients:readLectura de clientes OAuthclients:writeEscritura de clientes OAuthverifications:writeEscritura de verificacionespasswords:writeEscritura de contraseñaskeys:readLectura de clavessettings:readLectura de configuracionessettings:writeEscritura de configuraciones
or
stage
OAuth 2.0 Client Credentials Grant (RFC 6749 §4.4).
Utilice este flow para autenticación de aplicación-a-aplicación sin intervención del usuario.
Ideal para servicios backend y integración entre sistemas.
clientCredentials Flow
Token URL
https://stage-api.kuenta.co/v1/oauth/tokenScopes:
*Acceso completo (administrador)users:readLectura de usuariosusers:writeEscritura de usuariosusers.info:writeEscritura de información de usuariosclients:readLectura de clientes OAuthclients:writeEscritura de clientes OAuthverifications:writeEscritura de verificacionespasswords:writeEscritura de contraseñaskeys:readLectura de clavessettings:readLectura de configuracionessettings:writeEscritura de configuraciones
or
demo
OAuth 2.0 Client Credentials Grant (RFC 6749 §4.4).
Utilice este flow para autenticación de aplicación-a-aplicación sin intervención del usuario.
Ideal para servicios backend y integración entre sistemas.
clientCredentials Flow
Token URL
https://demo-api.kuenta.co/v1/oauth/tokenScopes:
*Acceso completo (administrador)users:readLectura de usuariosusers:writeEscritura de usuariosusers.info:writeEscritura de información de usuariosclients:readLectura de clientes OAuthclients:writeEscritura de clientes OAuthverifications:writeEscritura de verificacionespasswords:writeEscritura de contraseñaskeys:readLectura de clavessettings:readLectura de configuracionessettings:writeEscritura de configuraciones
or
prod
OAuth 2.0 Client Credentials Grant (RFC 6749 §4.4).
Utilice este flow para autenticación de aplicación-a-aplicación sin intervención del usuario.
Ideal para servicios backend y integración entre sistemas.
clientCredentials Flow
Token URL
https://api.kuenta.co/v1/oauth/tokenScopes:
*Acceso completo (administrador)users:readLectura de usuariosusers:writeEscritura de usuariosusers.info:writeEscritura de información de usuariosclients:readLectura de clientes OAuthclients:writeEscritura de clientes OAuthverifications:writeEscritura de verificacionespasswords:writeEscritura de contraseñaskeys:readLectura de clavessettings:readLectura de configuracionessettings:writeEscritura de configuraciones
Parameters
Header Parameters
Config-Organization-ID*
ID de configuración de la organización.
Formato: UUID v4
Typestring
RequiredExample
123e4567-e89b-12d3-a456-426614174000format
uuidOrganization-ID*
ID de la organización que está importando el crédito.
Formato: UUID v4
Typestring
RequiredExample
123e4567-e89b-12d3-a456-426614174000format
uuidRequest Body
application/json
El teléfono no existe en Kuenta, así que se crea el deudor a partir de
profile. Tasa 25,045 % EA (≈ 1,88 % mensual), mora 30,5 % EA explícita, y
dos pagos históricos en orden ascendente.
JSON
{
"creditLineID": "456e7890-e89b-12d3-a456-426614174001",
"externalID": "LEGACY-CRED-000123",
"phone": "+573001234567",
"document": "1020304050",
"documentType": 1,
"principal": 1000000,
"rate": "25.0452604625315",
"debtRate": "30.5",
"timeDays": 360,
"frequency": 30,
"startedAt": "2025-03-11",
"status": 7,
"profile": {
"firstName": "Ana María",
"lastName": "Rodríguez Pérez",
"idNumber": "1020304050",
"idType": "1",
"birthdate": "1990-05-14",
"addressHome": "Calle 123 # 45-67 Apto 802",
"neighborhoodHome": "Chapinero Alto",
"occupation": "Comerciante",
"incomeMonthly": 4500000
},
"payments": [
{
"paymentDate": "2025-04-11",
"paymentValue": 120000
},
{
"paymentDate": "2025-05-12",
"paymentValue": 120000
}
]
}
Responses
Crédito importado (o reutilizado, si el `externalID` ya existía).
La respuesta usa el sobre **JSend**: `status: "success"` y los datos del crédito
bajo `data.import_credit`.
**Nota:** el encabezado `location` de esta respuesta trae la cadena literal
`credit imported`, no una URL. No lo use para navegar al recurso.
application/json
JSON
{
"status": "success",
"data": {
"import_credit": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"external_id": "LEGACY-CRED-000123",
"reference": "K7XQ2M"
}
}
}