Libro de reclamaciones
La puerta pública sin credencial, el formulario que hay que publicar y los quince días hábiles.
El libro de reclamaciones virtual que el Código de Protección y Defensa del Consumidor exige a todo establecimiento comercial. Aquí lo publicas, lo recibes y lo contestas.
Es la única superficie de esta plataforma sin credencial: quien presenta un reclamo es un consumidor, no un integrador, y no tiene ni tendrá cuenta aquí.
Las tres piezas
| Pieza | Quién la usa | Dónde vive |
|---|---|---|
| El libro | Tú, una vez | POST /api/v2/complaint-books |
| El formulario | El consumidor, sin credencial | Tu web → POST /public/v1/complaint-books/{bookId}/entries |
| La respuesta | Alguien de tu negocio | La consola, en «Reclamaciones» |
Uno: acuña el libro
curl -X POST https://apifact.fac-360.com/api/v2/complaint-books \
-H "Authorization: Bearer $APIFACT_TOKEN"Devuelve un bookId y la intakeUrl a la que tu formulario tiene que enviar. Necesita el scope
complaints:manage.
El identificador del libro ES la capacidad. Es un UUID opaco que acuñamos nosotros: quien lo
tenga puede escribir un reclamo en ese libro y nada más. No puede leer ninguno, no puede saber de
quién es el libro, y no puede enumerar empresas probando RUC — que es exactamente lo que un campo
ruc en el cuerpo habría permitido.
Si el identificador acaba donde no debe, se rota: POST /api/v2/complaint-books/rotation. La
rotación exige nombrar el que se reemplaza, porque repetirla dos veces dejaría publicada en tu web
una dirección que ya no existe.
Dos: publica el formulario
El reglamento pide que el libro virtual sea accesible desde la portada de tu web. Esto es un formulario completo con los campos que exige, listo para pegar y darle estilo.
<form id="libro" novalidate>
<fieldset>
<legend>Tipo</legend>
<label
><input type="radio" name="kind" value="RECLAMO" checked /> Reclamo · disconformidad con el
producto o servicio</label
>
<label
><input type="radio" name="kind" value="QUEJA" /> Queja · disconformidad con la
atención</label
>
</fieldset>
<fieldset>
<legend>Tus datos</legend>
<label>Nombre completo <input name="name" required maxlength="200" /></label>
<label>
Tipo de documento
<select name="documentType" required>
<option value="DNI">DNI</option>
<option value="CE">Carné de extranjería</option>
<option value="PASAPORTE">Pasaporte</option>
<option value="RUC">RUC</option>
</select>
</label>
<label>Número <input name="documentNumber" required maxlength="20" /></label>
<label>Correo <input name="email" type="email" maxlength="200" /></label>
<label>Teléfono <input name="phone" maxlength="40" /></label>
<label>Dirección <input name="address" maxlength="300" /></label>
<label><input name="isMinor" type="checkbox" /> Soy menor de edad</label>
<label>Nombre del padre, madre o apoderado <input name="guardianName" maxlength="200" /></label>
</fieldset>
<fieldset>
<legend>Sobre qué reclamas</legend>
<label>
¿Producto o servicio?
<select name="itemKind" required>
<option value="PRODUCTO">Producto</option>
<option value="SERVICIO">Servicio</option>
</select>
</label>
<label>Descripción <input name="itemDescription" required maxlength="300" /></label>
<label>Monto reclamado (S/) <input name="amount" type="number" step="0.01" min="0" /></label>
</fieldset>
<fieldset>
<legend>Tu reclamo</legend>
<label>Detalle <textarea name="detail" required maxlength="4000" rows="6"></textarea></label>
<label>Qué pides <textarea name="requested" maxlength="2000" rows="3"></textarea></label>
</fieldset>
<button type="submit">Presentar</button>
<p id="resultado" role="status" aria-live="polite"></p>
</form>Y el envío. Nótese que el cuerpo va anidado, no plano:
const LIBRO = "https://apifact.fac-360.com/public/v1/complaint-books/TU-BOOK-ID/entries";
document.getElementById("libro").addEventListener("submit", async (event) => {
event.preventDefault();
const form = new FormData(event.target);
const salida = document.getElementById("resultado");
const texto = (campo) => String(form.get(campo) ?? "").trim() || undefined;
const cuerpo = {
kind: form.get("kind"),
consumer: {
name: texto("name"),
documentType: form.get("documentType"),
documentNumber: texto("documentNumber"),
email: texto("email"),
phone: texto("phone"),
address: texto("address"),
isMinor: form.get("isMinor") === "on",
guardianName: texto("guardianName"),
},
item: {
kind: form.get("itemKind"),
description: texto("itemDescription"),
amount: form.get("amount") ? Number(form.get("amount")) : undefined,
},
detail: texto("detail"),
requested: texto("requested"),
};
const respuesta = await fetch(LIBRO, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(cuerpo),
});
const datos = await respuesta.json();
if (respuesta.status === 201) {
salida.textContent =
`Presentado. Tu código es ${datos.trackingCode}. Guárdalo: lo necesitarás para ` +
`hacer seguimiento o para citarlo ante INDECOPI.`;
event.target.reset();
return;
}
// El código del error dice qué pasó; el mensaje está escrito para que lo lea una persona.
salida.textContent = datos.error?.message ?? "No se pudo presentar. Inténtalo de nuevo.";
});Enseña el código de seguimiento y di para qué sirve. El 201 devuelve un trackingCode como
R-2026-000001. No hay ninguna ruta pública que lo consulte —un identificador de libro filtrado
no puede leer un reclamo, y esa es la razón— así que el código le sirve al consumidor para
reclamártelo a ti y para citarlo ante INDECOPI. Un código entregado sin explicación es un código
que se tira.
Tres: contéstalo
Desde la consola, en Reclamaciones. Cada fila abre el reclamo entero y el campo de respuesta.
El plazo es de quince días hábiles desde la presentación, y la consola cuenta los transcurridos por ti. No hay fecha límite guardada en ninguna parte: se deriva, así que no puede desincronizarse.
Quien contesta reclamos suele ser alguien del negocio y no un desarrollador, y por eso esta parte
vive en la consola. Si aun así lo prefieres por API, es POST /api/v2/complaints/{trackingCode}/response
con el scope complaints:respond.
Lo que no se puede hacer, y por qué
- Editar un reclamo. Lo que el consumidor presentó es el registro. Tu respuesta va al lado, nunca encima, y nadie —tampoco nosotros— puede cambiar ese texto.
- Borrarlo. Se conserva dos años desde la presentación, que es lo que manda el reglamento, y después se anonimiza solo.
- Responder dos veces. Hay una respuesta por reclamo. Volver a enviarla la corrige, y la fecha sigue siendo la de la primera: una respuesta tardía no puede disfrazarse de puntual cambiándole el texto.
La prueba de humanidad
Un formulario público sin protección acaba recibiendo basura. Si te pasa, enciende Turnstile:
curl -X POST https://apifact.fac-360.com/api/v2/complaint-books/protection \
-H "Authorization: Bearer $APIFACT_TOKEN" \
-H "content-type: application/json" \
-d '{"requiresTurnstile": true}'A partir de ahí tu formulario tiene que enviar el token en el cuerpo, como turnstileToken.
Enciéndelo después de que tu formulario lo envíe, no antes. Un libro que exige la comprobación recibiendo peticiones sin token las rechaza: aceptar sin comprobar sería servir una promesa que nadie cumple.
Límite y errores
La puerta pública admite diez presentaciones por minuto y por dirección IP. Es aproximado y por localización: si lo pruebas en serie no lo dispararás, y eso no significa que no esté.
| Código | Qué pasó |
|---|---|
COMPLAINT_BOOK_NOT_AVAILABLE | Identificador desconocido, libro retirado o empresa inactiva |
COMPLAINT_INVALID | Faltan campos obligatorios o alguno pasa de su largo |
COMPLAINT_RATE_LIMITED | Diez por minuto desde esa IP |
COMPLAINT_TURNSTILE_REQUIRED | El libro exige la comprobación y no llegó token |
Todo libro inutilizable contesta el mismo 404. Identificador que no existe, libro retirado,
empresa dada de baja: un código y un mensaje. Distinguirlos convertiría esta puerta en una forma
de averiguar qué identificadores son válidos.