API para desarrolladores
Conecta Puli con tu software, Zapier o Make.
Una API REST sencilla para crear clientes, emitir facturas con VeriFactu, registrar cobros y gastos, y webhooks firmados que te avisan cuando pasa algo. Disponible en los planes Premium y Empresarial.
Autenticación
Crea una clave en Ajustes → API y webhooks. La clave se muestra una sola vez; guárdala como un secreto. Cada petición lleva la cabecera Authorization. Una clave de «solo lectura» no puede crear ni modificar nada.
curl https://puli.es/api/v1/me \
-H "Authorization: Bearer puli_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"- Base:
https://puli.es/api/v1 - Formato: JSON en UTF-8; fechas ISO 8601 (
2026-10-13); importes en euros; IVA en %. - Límite: 120 peticiones por minuto y clave. Pasado,
429conRetry-After. - Paginación:
limite(máx. 100) ycursor; la respuesta traesiguienteCursoronull.
Errores
Siempre con la misma forma: { "error": { "codigo": "…", "mensaje": "…" } }. Códigos: 400 datos_invalidos, 401 no_autenticado / clave_invalida, 402 plan_insuficiente, 403 sin_permiso, 404 no_encontrado, 409 conflicto (p. ej. límite del plan o factura ya emitida), 429 limite_peticiones.
Endpoints
| Método | Ruta | Qué hace | Permiso |
|---|---|---|---|
| GET | /me | Empresa, plan y permisos de la clave. Úsalo para probar la conexión. | lectura |
| GET | /clientes?q=&nif=&limite=&cursor= | Lista clientes (búsqueda por texto o NIF). | lectura |
| POST | /clientes | Crea un cliente. | escritura |
| GET | /clientes/{id} | Un cliente. | lectura |
| PATCH | /clientes/{id} | Cambia solo los campos que envíes. | escritura |
| DELETE | /clientes/{id} | Elimina un cliente. | escritura |
| GET | /productos?q=&activos=1 | Lista productos y servicios. | lectura |
| POST | /productos | Crea un producto (precio base sin IVA). | escritura |
| GET | /productos/{id} | Un producto. | lectura |
| PATCH | /productos/{id} | Actualiza un producto. | escritura |
| DELETE | /productos/{id} | Elimina un producto. | escritura |
| GET | /productos/{id}/movimientos | Movimientos de stock del producto (ventas, compras, ajustes…). | lectura |
| POST | /productos/{id}/movimientos | Mueve stock: `entrada`, `salida`, `ajuste` (con signo), `recuento` o `traspaso`. | escritura |
| GET | /almacenes | Tus almacenes (el principal siempre existe). | lectura |
| GET | /facturas?estado=&desde=&hasta=&clienteId=&limite=&cursor= | Lista facturas, de la más reciente a la más antigua. | lectura |
| POST | /facturas | Crea un borrador. Con `"emitir": true` se numera y registra en VeriFactu en el acto. | escritura |
| GET | /facturas/{id} | Una factura con líneas, totales, pagos y estado VeriFactu. | lectura |
| POST | /facturas/{id}/emitir | Emite un borrador (número de serie y registro VeriFactu). | escritura |
| POST | /facturas/{id}/pagos | Registra un cobro. Si cubre el saldo, la factura pasa a pagada. | escritura |
| GET | /facturas/{id}/pdf | El PDF con el diseño de tu empresa. | lectura |
| GET | /presupuestos?estado=&desde=&hasta= | Lista presupuestos. | lectura |
| POST | /presupuestos | Crea un presupuesto numerado. | escritura |
| GET | /presupuestos/{id} | Un presupuesto. | lectura |
| GET | /gastos?categoria=&desde=&hasta= | Lista gastos. | lectura |
| POST | /gastos | Crea un gasto (importe + IVA o líneas). | escritura |
| GET | /gastos/{id} | Un gasto. | lectura |
| GET | /proyectos?estado= | Lista proyectos con su resumen (horas, sin facturar, facturado, rentabilidad). | lectura |
| POST | /proyectos | Crea un proyecto (nombre, clienteId, tarifaHora, presupuesto…). | escritura |
| GET | /proyectos/{id} | Un proyecto con sus horas. | lectura |
| PATCH | /proyectos/{id} | Cambia datos o estado del proyecto. | escritura |
| POST | /proyectos/{id}/horas | Registra horas (minutos, fecha, descripción, facturable). | escritura |
| POST | /proyectos/{id}/facturar | Crea la factura con las horas pendientes (y gastos si `incluirGastos`); con `emitir` la numera. | escritura |
| GET | /webhooks | Tus webhooks. | lectura |
| POST | /webhooks | Crea un webhook; devuelve el secreto una sola vez. | escritura |
| PATCH | /webhooks/{id} | Cambia URL, eventos o activo. | escritura |
| DELETE | /webhooks/{id} | Elimina un webhook. | escritura |
| POST | /webhooks/{id}/prueba | Envía un evento `webhook.prueba` firmado. | escritura |
| GET | /webhooks/{id}/entregas | Últimas entregas con código HTTP y reintentos. | lectura |
Ejemplo: crear y emitir una factura
Necesitas el id de un cliente (créalo con POST /clientes o búscalo por NIF). El servidor calcula los totales, asigna el número de serie y registra la factura en VeriFactu.
curl -X POST https://puli.es/api/v1/facturas \
-H "Authorization: Bearer puli_live_…" \
-H "Content-Type: application/json" \
-d '{
"clienteId": "9D1F…",
"fechaExpedicion": "2026-10-13",
"descripcion": "Servicios de octubre",
"lineas": [
{ "concepto": "Diseño web", "cantidad": 10, "precioUnitario": 60, "iva": 21 },
{ "productoId": "3F1B…", "concepto": "Taza (del catálogo: descuenta stock al emitir)", "cantidad": 2, "precioUnitario": 4, "iva": 21 },
{ "concepto": "Hosting", "precioUnitario": 20, "iva": 21, "descuento": 10 }
],
"tipoIRPF": 15,
"emitir": true
}'Respuesta (resumida):
{
"factura": {
"id": "7C2A…", "serie": "A", "numero": "2026-0042", "numeroCompleto": "A-2026-0042",
"estado": "emitida", "fechaExpedicion": "2026-10-13T10:00:00.000Z",
"cliente": { "id": "9D1F…", "nombre": "Acme SL", "nif": "B12345678", "email": "admin@acme.es" },
"baseImponible": 618, "cuotaIVA": 129.78, "tipoIRPF": 15, "cuotaIRPF": 92.7, "total": 655.08,
"pagada": false, "saldoPendiente": 655.08,
"verifactu": { "estadoAeat": "pendiente_envio", "csv": null, "uuid": "…" },
"pdf": "https://puli.es/api/v1/facturas/7C2A…/pdf"
},
"verifactu": { "estadoAeat": "pendiente_envio", "csv": null }
}Para cobrarla: POST /facturas/{id}/pagos con { "importe": 655.08, "fecha": "2026-10-20", "metodoPago": "Transferencia" }.
Webhooks
Añade una URL https en Ajustes → API y webhooks (o con POST /webhooks) y elige los eventos. Puli hace un POST con JSON y reintenta hasta 6 veces si tu servidor no responde 2xx (al minuto, a los 5, a los 30, a las 2 h y a las 12 h). Tras 25 fallos seguidos el webhook se desactiva solo.
| Evento | Cuándo |
|---|---|
| factura.emitida | Una factura pasa de borrador a emitida, desde la web, el iPhone, Android o la API. |
| factura.pagada | Una factura queda cobrada: pago manual, enlace de pago, Tap to Pay o la API. |
| presupuesto.aceptado | El cliente acepta el presupuesto desde el enlace público o tú lo marcas aceptado. |
| gasto.creado | Se registra un gasto (a mano, por OCR, por email o por la API). |
| stock.bajo_minimo | Un producto con control de stock cae por debajo de su mínimo. |
Cuerpo de un evento:
POST https://tu-servidor.com/puli
Content-Type: application/json
X-Puli-Event: factura.pagada
X-Puli-Delivery: evt_8f3c…
X-Puli-Signature: t=1760349600,v1=3b2a…
{
"id": "evt_8f3c…",
"tipo": "factura.pagada",
"creado": "2026-10-13T10:00:00.000Z",
"intento": 1,
"datos": {
"factura": {
"id": "7C2A…", "numeroCompleto": "A-2026-0042", "estado": "pagada",
"cliente": { "nombre": "Acme SL", "nif": "B12345678", "email": "admin@acme.es" },
"total": 655.08, "totalPagado": 655.08, "saldoPendiente": 0, "pagada": true,
"fechaCobro": "2026-10-20T12:00:00.000Z",
"pdf": "https://puli.es/api/v1/facturas/7C2A…/pdf"
}
}
}Verificar la firma
La cabecera X-Puli-Signature lleva la marca de tiempo t y v1 = HMAC-SHA256(secreto, t + "." + cuerpo) en hexadecimal, sobre el cuerpo crudo tal como llega. Rechaza firmas de más de 5 minutos.
import { createHmac, timingSafeEqual } from "node:crypto";
export function firmaValida(secreto, cabecera, cuerpoCrudo) {
const partes = Object.fromEntries(cabecera.split(",").map((p) => p.split("=")));
const t = Number(partes.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
const esperado = createHmac("sha256", secreto).update(`${t}.${cuerpoCrudo}`).digest("hex");
const recibido = String(partes.v1 || "");
return recibido.length === esperado.length && timingSafeEqual(Buffer.from(recibido), Buffer.from(esperado));
}Zapier y Make
Recibir eventos de Puli
- En Zapier crea un Zap con el disparador Webhooks by Zapier → Catch Hook (en Make, el módulo Webhooks → Custom webhook).
- Copia la URL que te dan y añádela en Puli, en Ajustes → API y webhooks, con los eventos que quieras.
- Pulsa Probar en Puli: Zapier/Make recibirán un
webhook.pruebacon la estructura de ejemplo. - Usa
datos.factura.*en los pasos siguientes (una fila en Google Sheets, un correo, un apunte en tu CRM…).
Crear cosas en Puli
- Acción Webhooks by Zapier → Custom Request (Make: HTTP → Make a request).
- Método
POST, URLhttps://puli.es/api/v1/clienteso/facturas, cabecerasAuthorization: Bearer puli_live_…yContent-Type: application/json. - Cuerpo JSON como en los ejemplos de arriba. La respuesta trae el
idpara encadenar pasos.
¿Necesitas un endpoint que no está?
Cuéntanos qué quieres automatizar desde soporte. La API crece con lo que piden los clientes.
