Fac-360

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

PiezaQuién la usaDónde vive
El libroTú, una vezPOST /api/v2/complaint-books
El formularioEl consumidor, sin credencialTu web → POST /public/v1/complaint-books/{bookId}/entries
La respuestaAlguien de tu negocioLa 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ódigoQué pasó
COMPLAINT_BOOK_NOT_AVAILABLEIdentificador desconocido, libro retirado o empresa inactiva
COMPLAINT_INVALIDFaltan campos obligatorios o alguno pasa de su largo
COMPLAINT_RATE_LIMITEDDiez por minuto desde esa IP
COMPLAINT_TURNSTILE_REQUIREDEl 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.

On this page