Saltar al contenido

API de ingesta

Todo lo que hace la llave lo puedes hacer tú: un POST a /api/event con un JSON pequeño. Este es el contrato completo.

El endpoint

POST https://pisi.to/api/event
http

Un único endpoint, público y con CORS abierto: cualquier web puede mandarle eventos, y el filtro es el dominio. Acepta text/plain, que es lo que manda sendBeacon y no dispara preflight, y application/json. No hay autenticación: si el dominio del payload no es de ningún piso, el evento se tira.

Respuestas

Responde antes de procesar, así que el 202 significa «recibido», no «guardado». Un evento de un dominio desconocido, uno filtrado como bot o uno por encima del límite se descartan en silencio con el mismo 202: el navegador del visitante nunca ve un error por nuestra culpa.

CódigoCuándo
202Siempre que el cuerpo se pueda leer. La respuesta va sin cuerpo.
400El cuerpo está vacío, no es JSON válido o le falta un campo obligatorio (n, u o d).
413El cuerpo pasa de 8 KB. Una URL de 4.000 caracteres más un referrer de 2.000 más props caben de sobra.
202Límite de 60 peticiones por segundo desde la misma IP al mismo dominio. Por encima no hay error: se responde 202 y el evento se descarta.

El payload

Un objeto JSON plano. Solo n, u y d son obligatorios; el resto es lo que la llave lee del navegador, y puedes omitirlo si no lo tienes.

CampoTipoQué es
nObligatoriostringNombre del evento, hasta 300 caracteres. pageview es una habitación vista; cualquier otro nombre es un timbre.
uObligatoriostringURL completa de la página, hasta 4.000 caracteres. El servidor se queda con el host y la ruta, y tira la query salvo los parámetros utm.
dObligatoriostringDominio del piso, sin protocolo ni www. Admite varios separados por comas (máximo 10) para mandar el mismo evento a varios pisos. Si ninguno es de un piso tuyo, el evento se descarta.
rstringReferrer, hasta 2.000 caracteres. Vacío o ausente cuenta como tráfico directo.
w / hnumberAncho y alto de la ventana en píxeles. Sirven para el tipo de dispositivo y el desglose por tamaño de pantalla.
tnumberPuntos táctiles del dispositivo (navigator.maxTouchPoints). Distingue un iPad que se anuncia como Mac.
lstringIdioma del navegador (navigator.language), hasta 35 caracteres.
pobjectPropiedades del evento: hasta 30 claves, cada clave hasta 300 caracteres y cada valor hasta 2.000. Solo cadenas, números y booleanos. Lo que sobra se recorta en silencio; el evento nunca se rechaza por una prop.
${ currency, amount }Ingresos del evento: currency de tres letras (ISO 4217) y amount como número o como cadena. Se ven en el desglose de timbres.
enumberSegundos con la pestaña visible. Solo tiene sentido en el evento engagement.
sdnumberScroll máximo alcanzado, de 0 a 100. Solo en engagement. Por compatibilidad también se acepta como s.

Nombres reservados

Seis nombres tienen significado propio en el panel. Los cuatro últimos puedes mandarlos tú desde la API cuando la llave no llega, por ejemplo un formulario que se procesa en el servidor. Los dos primeros son las métricas base: mandarlos a mano solo tiene sentido con la variante manual, y si lo haces, sabes que estás contando visitas.

NombreQué significa
pageviewUna habitación vista. Cuenta como página vista y abre o alarga la visita.
engagementTiempo visible y scroll de la página que se deja. Lleva e y sd y se suma a la visita en curso; nunca abre una visita nueva.
404Página no encontrada. Sale en su propio desglose para que veas los enlaces rotos.
Outbound Link: ClickClick en un enlace a otro host. Prop url.
File DownloadDescarga de un fichero. Prop url.
Form: SubmitEnvío de un formulario.

Ejemplos

Los dos ejemplos mandan el mismo timbre. Cambia example.com por tu dominio.

Con curl

El User-Agent es a propósito: el filtro de bots descarta el de curl, así que sin uno de navegador recibirás un 202 y el evento no aparecerá en el panel.

curl -X POST https://pisi.to/api/event \
  -H "Content-Type: application/json" \
  -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" \
  -d '{"n":"Alta","u":"https://example.com/registro","d":"example.com","p":{"plan":"pro"}}'
bash

Con fetch, desde el navegador

Es lo que hace la llave por dentro, sin las mil cosas que resuelve ella: rutas de SPA, bfcache, deduplicación, exclusión del dueño. Si puedes poner el script, ponlo.

fetch("https://pisi.to/api/event", {
  method: "POST",
  // text/plain evita el preflight CORS; application/json también vale.
  headers: { "Content-Type": "text/plain" },
  keepalive: true,
  body: JSON.stringify({
    n: "Alta",
    u: location.href,
    d: "example.com",
    r: document.referrer,
    w: innerWidth,
    h: innerHeight,
    t: navigator.maxTouchPoints,
    l: navigator.language,
    p: { plan: "pro" },
  }),
});
js

Desde el servidor

Mismo endpoint, mismo payload. La diferencia es que la IP y el User-Agent se leen de la petición que llega a pisi.to, y desde tu servidor esa petición la hace tu servidor, no el visitante. Tres cosas a tener en cuenta:

  • Reenvía el User-Agent del visitante. El de tu servidor (node-fetch, php-curl, python-requests) se filtra como bot y el evento se descarta.
  • Sin más, la IP es la de tu servidor: todos los visitantes salen de la misma ciudad y cuentan como uno solo al día. Para eventos sueltos (una compra, un alta) suele dar igual; para páginas vistas, no.
  • Con la clave del piso en X-Pisito-Key, la ingesta se cree el X-Forwarded-For que mandes y el visitante se atribuye bien. Hoy la clave la configuramos nosotros: pídela a hola@pisi.to. Cuando llegue la verificación de dominio la verás en los ajustes del piso.

Node

// Un handler cualquiera: Express, Hono, una route de Next…
export async function trackSignup(req, plan) {
  await fetch("https://pisi.to/api/event", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      // El filtro de bots mira el User-Agent: el de tu servidor no es un navegador.
      "User-Agent": req.headers["user-agent"] ?? "",
      // Con la clave del piso, la IP del visitante manda para el país.
      "X-Forwarded-For": req.ip,
      "X-Pisito-Key": process.env.PISITO_KEY,
    },
    body: JSON.stringify({
      n: "Alta",
      u: "https://example.com/registro",
      d: "example.com",
      l: req.headers["accept-language"]?.split(",")[0],
      p: { plan },
    }),
  });
}
js

PHP

<?php
$payload = json_encode([
  'n' => 'Compra',
  'u' => 'https://example.com/gracias',
  'd' => 'example.com',
  'p' => ['plan' => 'pro'],
  '$' => ['currency' => 'EUR', 'amount' => '29.90'],
]);

$ch = curl_init('https://pisi.to/api/event');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_POSTFIELDS => $payload,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_TIMEOUT => 2,
  CURLOPT_HTTPHEADER => [
    'Content-Type: application/json',
    'User-Agent: ' . ($_SERVER['HTTP_USER_AGENT'] ?? ''),
    'X-Forwarded-For: ' . ($_SERVER['REMOTE_ADDR'] ?? ''),
    'X-Pisito-Key: ' . getenv('PISITO_KEY'),
  ],
]);
curl_exec($ch);
curl_close($ch);
php

Si lo que quieres es que la llave llegue aunque haya bloqueadores, no necesitas nada de esto: sirve el script y el endpoint desde tu dominio con un proxy.

Ver las guías de proxy

Lo que no existe todavía

Para que no lo busques:

  • No hay API de lectura de estadísticas. Está en camino. Mientras tanto, cualquier tabla del panel se exporta a CSV con el periodo y los filtros que tengas puestos, y los paneles públicos sirven para compartir sin dar acceso.
  • No hay importador de histórico desde otras herramientas. También en camino.

Cuando existan, saldrán en novedades y aquí.

¿Se te resiste?

Escríbenos a hola@pisi.to con la URL de tu piso y le echamos un ojo.