Un proyecto tiene una configuración de flujo: qué pasos corre, en qué orden, con qué parámetros y qué hacer cuando algo falla. Se arma desde el dashboard; esta página documenta el modelo que hay abajo, para que sepas exactamente qué significa cada opción.
Versionado y congelamiento
Cada vez que publicás, se crea una versión (cfg_00000003) que queda
inmutable. Cada sesión se ata a la versión activa en su modo al momento de
crearse.
Consecuencias que conviene tener presentes:
- cambiar el flujo no afecta a las sesiones en curso ni a las ya resueltas;
- el costo en tokens se congela con la versión: una sesión se cobra al precio vigente cuando se publicó ese flujo;
- las opciones de payload de webhook también se leen de la versión congelada de esa sesión, no de la configuración actual del proyecto;
- un proyecto tiene punteros independientes para
testy paralive: podés probar una versión nueva sin tocar producción.
Estructura de una configuración
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
name | string | sí | Nombre del flujo, 1 a 120 caracteres. |
steps | array | sí | Definición de los pasos disponibles. Cada uno con id kebab-case único, tipo y parámetros. |
flow.sequence | string[] | sí | Orden de ejecución. Al menos un paso. |
flow.rules | array | no | Reglas condicionales. Ver más abajo. |
expiration.linkTtlHours | number | no | Vigencia por defecto del link: de 0.25 a 168 horas. Por defecto 24. Se puede pisar por sesión con expires_in_hours. |
expiration.sessionIdleMinutes | integer | no | Inactividad tolerada dentro del flujo antes de darlo por abandonado: de 1 a 1440 minutos. Por defecto 30. |
review.enabled | boolean | no | Si los casos ambiguos van a revisión manual. Por defecto activado. |
review.slaHours | number | no | Objetivo de resolución de la revisión: de 1 a 720 horas. Por defecto 24. |
webhookEvents | string[] | no | Qué eventos dispara este proyecto. Vacío significa que no dispara ninguno. |
webhookPayload | object | no | Qué datos adicionales viajan en los webhooks. Todo desactivado por defecto. |
retentionDays | integer | no | Retención de la evidencia: de 30 a 1825 días. Por defecto 1825 (5 años). |
branding | object | no | Logo, color primario, nombre de la app, idioma (por defecto es-AR) y textos personalizados. |
redirects | object | no | URLs a las que mandar a la persona al terminar: onApproved, onRejected, onExpired. |
webhookPayload
Cuatro interruptores, todos apagados por defecto. Controlan qué sale en los eventos — ver Eventos y payload.
| Campo | Tipo | Habilita | Descripción |
|---|---|---|---|
documentData | boolean | result.document | Datos extraídos del documento. |
verifiedChannels | boolean | result.verified_channels | Email y teléfono verificados por OTP. |
formData | boolean | result.form_data | Respuestas del formulario. |
evidenceLinks | boolean | evidence_links | Links firmados a las imágenes capturadas, con 5 minutos de vida. |
Parámetros de cada paso
document_ar
| Campo | Tipo | Por defecto | Descripción |
|---|---|---|---|
acceptedDocs | string[] | — | Al menos uno de dni_card, passport, driver_license. |
allowScreenCapture | boolean | false | Si se acepta la foto de una pantalla. Activarlo degrada la detección de pantalla de rechazo duro a señal blanda. |
rejectExpired | boolean | true | Si un documento vencido se rechaza. |
padronCrossCheck | boolean | false | Si se cruza contra el padrón oficial. |
padronUnavailablePolicy | enum | review | Qué hacer si el padrón no responde: approve, review o reject. |
nameMatchThreshold | number | 0.85 | Similitud mínima de nombres, entre 0 y 1. Por debajo del piso duro rechaza; entre el piso y este valor, señal blanda. |
maxCaptureAttempts | integer | 3 | Intentos de captura antes de dar el paso por perdido. De 1 a 10. |
face_liveness
| Campo | Tipo | Por defecto | Descripción |
|---|---|---|---|
challenge | enum | FaceMovementAndLightChallenge | FaceMovementAndLightChallenge (el de mayor precisión, con secuencia de luces) o FaceMovementChallenge. |
livenessThreshold | number | 85 | Umbral de vivacidad, acotado a [70, 99]. Por debajo de 70 la prueba se vuelve decorativa; 100 la haría imposible. |
compareFaces | object | ausente | Si falta, el paso corre en modo sólo prueba de vida. Si está, define las bandas del face match. |
compareFaces.autoApprove | number | — | Similitud a partir de la cual se aprueba sin intervención. Entre 70 y 100. |
compareFaces.reviewBand | [number, number] | — | Banda [mín, máx] que va a revisión manual. Cada extremo entre 70 y 100, con mín ≤ máx y autoApprove ≥ máx. |
maxAttempts | integer | 3 | Intentos de prueba de vida. De 1 a 5. |
otp_email
| Campo | Tipo | Por defecto | Descripción |
|---|---|---|---|
ttlMinutes | integer | 10 | Vigencia del código. De 1 a 90 minutos. |
maxAttempts | integer | 5 | Intentos de ingreso. De 1 a 10. |
maxResends | integer | 3 | Reenvíos permitidos. De 0 a 10. |
collectIfMissing | boolean | false | Si se le pide el email a la persona cuando no viene precargado. |
otp_whatsapp
| Campo | Tipo | Por defecto | Descripción |
|---|---|---|---|
ttlMinutes | integer | 5 | Vigencia del código. De 1 a 90 minutos. |
maxAttempts | integer | 5 | Intentos de ingreso. De 1 a 10. |
maxResends | integer | 3 | Reenvíos permitidos. De 0 a 10. |
fallbackToEmail | boolean | false | Si se cae a email cuando el envío por WhatsApp no prospera. |
collectIfMissing | boolean | false | Si se le pide el teléfono a la persona cuando no viene precargado. |
form
| Campo | Tipo | Por defecto | Descripción |
|---|---|---|---|
fields | array | — | Al menos un campo. |
fields[].key | string | — | Clave alfanumérica sin espacios, arrancando con letra. Es la clave del resultado en result.form_data. Única dentro del paso. |
fields[].label | string | — | Etiqueta visible. De 1 a 200 caracteres. |
fields[].kind | enum | — | text, select, date, number o checkbox. |
fields[].required | boolean | false | Si el campo es obligatorio. |
fields[].maxLength | integer | sin límite | Largo máximo. De 1 a 10000. |
fields[].options | string[] | — | Obligatorio en los campos select; al menos una opción. |
choice
| Campo | Tipo | Por defecto | Descripción |
|---|---|---|---|
options | string[] | — | Ids de los pasos que son opciones de este grupo. Un paso que es opción no puede estar además en flow.sequence. |
userSelects | boolean | true | Si elige la persona o el flujo. No cambia cuántas opciones corren. |
minRequired | integer | 1 | Cuántas opciones hay que completar. No puede superar la cantidad de opciones ofrecidas. |
Reglas condicionales
Una regla dice: cuando el paso X termina con el resultado R, hacé A.
{
"when": { "step": "documento", "result": "review" },
"then": { "goto": "verificacion-extra", "resume": true }
}
when
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
step | string | sí | Id del paso que dispara la regla. |
result | enum | sí | approved, rejected, review o retries_exhausted (el paso agotó sus reintentos). |
then
Exactamente una de estas cinco acciones:
| Campo | Tipo | Acción | Descripción |
|---|---|---|---|
goto | string | saltar | Salta al paso indicado. Con resume: true, al terminar ese paso el flujo retoma la continuación del paso que disparó la regla; sin resume, sigue desde donde esté el destino en la secuencia. |
continue | true | seguir | El flujo sigue pese al resultado, pero el resultado sigue contando para la decisión final. Es "seguí, ya vamos a decidir al final". |
skip | true | tolerar | Descarta el paso: su resultado no cuenta para la decisión final. Es "este validador es opcional". |
reject | true | terminar | Termina la sesión como rechazada, en el acto. |
review | true | terminar | Termina la sesión en revisión manual, en el acto. |
Pasos condicionales
Un paso puede declararse con entrada conditional: entonces sólo es
alcanzable por una regla goto, y no puede estar en flow.sequence. Es la forma
de definir ramas de excepción —una verificación extra que sólo corre cuando algo
salió raro— sin que se ejecuten en el camino feliz.
Qué valida la plataforma al publicar
Además del schema, se valida la estructura del grafo. Una configuración que no pasa estas reglas no se puede publicar:
| Campo | Tipo | Error | Descripción |
|---|---|---|---|
Referencias inexistentes | Estructura | unknown_step_ref | La secuencia, una regla o una opción de choice apunta a un id que no existe. |
Ids duplicados | Estructura | duplicate_step_id | Dos pasos con el mismo id. |
Opción en la secuencia | Estructura | choice_option_in_sequence | Un paso que es opción de un choice no puede estar además en flow.sequence. |
Condicional en la secuencia | Estructura | conditional_in_sequence | Un paso con entrada conditional sólo se alcanza por goto. |
Paso inalcanzable | Estructura | unreachable_step | Un paso definido que no está en la secuencia, ni es opción de un choice, ni es destino de una regla. |
Ciclo | Estructura | cycle_detected | El grafo de avance tiene un ciclo. Incluye el caso de un goto hacia un paso anterior de la secuencia: el salto más el camino de secuencia cierra el bucle. |
Sin camino al final | Estructura | no_terminal_path | Desde algún paso alcanzable no hay forma de llegar al final del flujo. |
Choice vacío o mal armado | Ejecutabilidad | choice_empty, duplicate_choice_option, choice_min_required | Un choice sin opciones, con una opción repetida, o que exige completar más opciones de las que ofrece. |
Claves de formulario repetidas | Ejecutabilidad | duplicate_form_field_key | Dos campos con la misma key: las respuestas se pisarían entre sí. |
Un paso alcanzado por un goto sin resume tiene una exigencia extra: como
no hereda continuación, cada uno de los cuatro resultados posibles necesita su
propia regla que encamine el flujo (goto sin resume, reject o review). Si
sólo cubrís el caso de fallo, la rama "aprueba y sigue" queda sin salida y la
configuración se rechaza. La forma más simple de evitarlo es usar resume: true.
Ejemplo completo
Un flujo con documento y biometría, donde un documento dudoso suma una verificación por WhatsApp antes de decidir:
{
"schemaVersion": 2,
"version": 3,
"name": "Onboarding estándar",
"steps": [
{
"id": "documento",
"type": "document_ar",
"params": {
"acceptedDocs": ["dni_card", "passport"],
"rejectExpired": true,
"padronCrossCheck": true,
"padronUnavailablePolicy": "review",
"nameMatchThreshold": 0.85,
"maxCaptureAttempts": 3
}
},
{
"id": "selfie",
"type": "face_liveness",
"params": {
"challenge": "FaceMovementAndLightChallenge",
"livenessThreshold": 85,
"compareFaces": { "autoApprove": 92, "reviewBand": [80, 92] },
"maxAttempts": 3
}
},
{
"id": "otp-refuerzo",
"type": "otp_whatsapp",
"params": {
"entry": "conditional",
"ttlMinutes": 5,
"maxAttempts": 5,
"collectIfMissing": true
}
}
],
"flow": {
"sequence": ["documento", "selfie"],
"rules": [
{
"when": { "step": "documento", "result": "review" },
"then": { "goto": "otp-refuerzo", "resume": true }
},
{
"when": { "step": "selfie", "result": "retries_exhausted" },
"then": { "review": true }
}
]
},
"expiration": { "linkTtlHours": 48, "sessionIdleMinutes": 30 },
"review": { "enabled": true, "slaHours": 24 },
"webhookEvents": [
"verification_session.approved",
"verification_session.rejected"
],
"webhookPayload": { "documentData": true }
}
Con la matriz de costos por defecto, ese flujo presupuesta 12 tokens:
document_ar (5) + face_liveness (5) + el destino de la regla goto,
otp_whatsapp (2). El paso condicional se cobra aunque no siempre corra: el
presupuesto es de peor caso. Ver Prueba y producción.