Construya su propia integración
Su catálogo, sus plazas libres, sus pedidos, sus entradas y sus escaneos también los puede recoger otro programa, que además puede crear pedidos por sí mismo. Aquí está cómo funciona — primero en palabras sencillas, luego con el código.
- Dirección base
- https://passavo.eu/api/v1
- Versión actual
- 2026-09-20
- La descripción misma
- openapi.json
¿Qué es una API, y la necesita?
Una API es una puerta en la parte de atrás de su cuenta. Donde usted hace clic en la pantalla de gestión, un programa puede pasar por esa puerta para leer sus datos o escribir en ellos — sin nadie delante de una pantalla.
Solo la necesita cuando quiere algo que la pantalla de gestión no hace: cifras en su propio sistema, plazas libres en su propia web, entradas desde una caja que ya tiene. Si puede hacer clic, hacer clic es más rápido.
Quien construya esa integración — su desarrollador web, su programa de contabilidad, alguien que sepa programar — necesita dos cosas de usted: la dirección de abajo y una clave que usted mismo crea.
Qué puede construir
Las plazas libres en su propia web
Muestre en su página de inicio qué visitas tienen aún sitio esta semana, con su propio diseño y con cifras que son correctas en ese mismo momento.
Alimentar su contabilidad
Deje que su asesor o su programa de contabilidad recoja por sí mismo las ventas del mes pasado, en lugar de enviar una exportación cada mes.
Una pantalla en la entrada
Una tableta en el vestíbulo que muestra qué franja horaria empieza enseguida y cuántas plazas quedan libres. Basta una llamada por minuto.
Vender desde su propio sistema
Cree un pedido desde el programa en el que ya trabaja, envíe al comprador un enlace de pago y entérese por un webhook de que ha pagado.
La API forma parte del plan Pro
Las claves se crean en Ajustes → Acceso API. Esa pantalla aparece en su menú en cuanto su organización está en ese plan; el plan lo cambia usted mismo en su pantalla de gestión, en Suscripción.
Vea los planes →Primero probar sin riesgo
Junto a una clave normal (pv_live_) puede crear una clave de prueba (pv_test_). Trabaja con su catálogo, sus franjas horarias y su aforo reales, y también puede crear pedidos. Un pedido de prueba así nunca pasa por un proveedor de pago — una página de prueba simula el pago —, no cuenta en su facturación y se elimina a las 24 horas. Hasta entonces ocupa plazas reales, así que trabaje preferiblemente con un producto creado para ello. Lo que queda fuera del entorno de pruebas, como gestionar webhooks, lo rechazamos para una clave de prueba con el código sandbox.
Inicio rápido en cinco pasos
De cero a una integración que sabe que se ha pagado. Cada paso lleva minutos, no días.
-
Cree una clave
Abra Ajustes → Acceso API en su pantalla de gestión y cree una clave solo con los derechos que necesita. El token completo lo ve una vez; guárdelo como guarda una contraseña.
-
Haga su primera llamada
Pida GET /me con su clave en la cabecera Authorization. Recibe su organización, su plan y los derechos de su clave. Si esto funciona, el resto también.
-
Cree un pedido
Envíe las líneas que quiere vender a POST /orders, con una Idempotency-Key — aquí es obligatoria. Si la conexión se rompe a medio camino, la misma llamada repetida no produce un segundo pedido. El campo reserved_until indica hasta cuándo se le reservan las plazas.
-
Lleve al comprador al enlace de pago
Pida el enlace de pago con POST /orders/{id}/checkout, junto con la dirección a la que llega el comprador después de pagar. Mande al comprador a ese enlace, o muéstrelo en su propia página. El pago pasa por el proveedor que ya ha conectado; en cuanto se recibe, las entradas se crean y se envían.
-
Deje que un webhook le avise
Prepare una dirección en su propia web y regístrela. En cuanto algo se paga, se anula o se escanea, lo recibe allí — no tiene que seguir preguntando si ha pasado algo.
curl
curl "https://passavo.eu/api/v1/me" \
-H "Authorization: Bearer pv_live_YOUR_API_KEY" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->withToken('pv_live_YOUR_API_KEY')
->get('https://passavo.eu/api/v1/me');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/me', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer pv_live_YOUR_API_KEY',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/me", headers=headers)
data = response.json()["data"]
Autenticación
Cada petición lleva su clave como bearer token. Una clave pertenece a una sola organización: esa organización es todo el mundo de esa petición, y nunca puede recoger por error los datos de otra persona.
Authorization: Bearer pv_live_YOUR_API_KEY
Una clave normal empieza por pv_live_, una clave de entorno de pruebas por pv_test_. Ese prefijo no es un secreto sino una ayuda: quien lo vea en un registro sabe enseguida de dónde viene.
El token completo existe un momento, al crearlo. Después solo guardamos una huella irreversible (sha256). Perdido significa de verdad perdido: revoque la clave y cree una nueva.
Derechos
Una clave sin derechos no puede hacer nada. Es a propósito lo contrario de lo que cabría esperar: estas claves las reparten los clientes a herramientas de terceros, y entonces el valor por defecto debe ser «nada» y no «todo».
- read
- Consultarlo todo: sus sedes, productos, precios, plazas libres, descuentos, vales regalo y abonos, y también sus pedidos, entradas, compradores y los escaneos en la entrada.
- write
- Crear pedidos, cobrarlos, anularlos y reembolsarlos; anular o reenviar entradas; registrar la entrada de visitantes; emitir vales regalo y abonos.
- webhooks
- Gestionar sus propios destinos de webhook.
- external_payment
- Marcar como pagado un pedido que cuesta dinero sin proveedor de pago, por ejemplo tras una transferencia o un pago en efectivo (POST /orders/{id}/mark-paid). Un derecho aparte, junto a write, para dárselo solo a una caja o una contabilidad que gestiona usted mismo. Confirmar un pedido de cero euros también se puede sin él.
Cuando algo sale mal
Todo error tiene la misma forma, con un código fijo y un texto traducido. Lea el código y no el texto: el texto se puede reescribir, el código nunca.
{
"error": {
"code": "validation_failed",
"message": "…",
"details": {}
}
}
| Código | Estado | Cuándo |
|---|---|---|
| unauthenticated | 401 | Clave ausente, desconocida, revocada o caducada. |
| forbidden | 403 | La clave es válida, pero no puede hacer esto. |
| plan_required | 403 | El plan de esta organización no incluye la API. |
| sandbox | 403 | Una clave de prueba intenta algo fuera del entorno de pruebas, como gestionar webhooks. |
| not_found | 404 | No existe, o no existe dentro de esta organización. |
| validation_failed | 422 | La propia petición no es correcta: un filtro desconocido, una fecha no válida. |
| rate_limited | 429 | Demasiadas llamadas en un minuto. |
| conflict | 409 | La misma Idempotency-Key ya se usó para otra cosa. |
| server_error | 500 | Algo ha fallado por nuestra parte. |
| slot_unavailable | 409 | La franja horaria está llena, ya ha empezado o es desconocida, o no se eligió ninguna aunque el producto la requiere. |
| sold_out | 409 | El producto está agotado o ya no está a la venta. |
| discount_invalid | 422 | El código de descuento no existe, está agotado o no se aplica aquí. |
| payment_provider_missing | 409 | La organización no tiene ningún proveedor de pago conectado. |
| not_cancellable | 409 | El pedido no se puede cancelar en su estado actual. |
| not_refundable | 409 | No queda nada por reembolsar en este pedido, o se pagó fuera del proveedor de pago. |
| check_in_duplicate | 409 | Este código ya se registró en la entrada. |
| check_in_invalid | 404 | Este código es desconocido. |
| check_in_wrong_day | 409 | Este código es válido, pero no hoy. |
| check_in_cancelled | 409 | Este código pertenece a una entrada cancelada o reembolsada. |
Una fila de otra organización devuelve siempre not_found y nunca un mensaje que diga que no tiene acceso. Si esa diferencia fuese visible, cualquiera con una clave válida podría averiguar cuántos productos tiene el vecino recorriendo números.
Paginación, orden y filtros
Las listas llegan página a página. No pide la página tres sino lo que viene después de la página anterior: la respuesta lleva un cursor, y usted lo envía de vuelta para seguir leyendo. Así una lista nunca se salta filas cuando se añade algo mientras avanza.
| Parámetro | Qué hace |
|---|---|
| page[size] | Cuántas filas por página; 25 por defecto, 100 como máximo. |
| page[cursor] | El next_cursor de la respuesta anterior. Si está vacío, esa era la última página. |
| sort | Por qué campo se ordena. Un guion delante invierte el orden. |
| filter[…] | Filtrar por un campo que el endpoint permite. |
| include | Incluir relaciones en la misma respuesta, separadas por comas. |
Un parámetro que el endpoint no conoce es un error, no un silencio. Quien se equivoque al escribir el nombre de un filtro debe enterarse — y no recibir sin saberlo la lista entera sin filtrar.
En la respuesta no hay total a propósito: con decenas de miles de filas ese recuento cuesta más que la página misma.
{
"data": [ … ],
"meta": { "next_cursor": "…" }
}
Enviar dos veces, ejecutar una
Envíe una Idempotency-Key con cada llamada que crea o cambia algo — una cadena aleatoria y única. Si la misma llamada vuelve a entrar con la misma clave, recibe exactamente la misma respuesta sin que nada ocurra por segunda vez. En POST /orders es obligatoria.
Idempotency-Key: 6f1c0b4a-6b2f-4a1b-9c7e-1f2d3e4a5b6c
La misma clave con otro contenido da conflict y no pasa nada. Una clave guardada dura 24 horas y vale por organización.
Versiones
La versión está en la ruta (/api/v1) y solo cambia si hay una ruptura; una segunda versión funciona entonces al lado y no a través. Cada pequeño añadido se fecha en la cabecera Passavo-Version, hoy 2026-09-20.
Lo que puede añadirse dentro de esta versión: un campo, un endpoint, un nuevo valor de orden o de filtro, un nuevo código de error. Construya su integración de modo que un campo desconocido no la tumbe.
Lo que nunca ocurre dentro de esta versión: quitar o renombrar un campo, cambiar un tipo, dar a un código otro significado, o hacer obligatorio un parámetro.
Límite de llamadas
Una clave puede hacer 120 llamadas por minuto. El límite vale por clave y no por dirección, para que dos clientes detrás del mismo proveedor en la nube no se estorben.
Cada respuesta le dice cómo va en X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. Si se pasa, recibe rate_limited con un Retry-After al lado; espere entonces esos segundos.
Ejemplos de código
La misma petición en cuatro lenguajes, por grupo de endpoints. Sustituya YOUR_API_KEY por su propia clave — y no la ponga nunca en código que un visitante pueda descargar.
Cuenta
Quién soy y qué puedo hacer.
GET /api/v1/me
curl
curl "https://passavo.eu/api/v1/me" \
-H "Authorization: Bearer pv_live_YOUR_API_KEY" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->withToken('pv_live_YOUR_API_KEY')
->get('https://passavo.eu/api/v1/me');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/me', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer pv_live_YOUR_API_KEY',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/me", headers=headers)
data = response.json()["data"]
Catálogo
Ubicaciones, productos y precios.
GET /api/v1/events
curl
curl "https://passavo.eu/api/v1/events" \
-H "Authorization: Bearer pv_live_YOUR_API_KEY" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->withToken('pv_live_YOUR_API_KEY')
->get('https://passavo.eu/api/v1/events');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/events', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer pv_live_YOUR_API_KEY',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/events", headers=headers)
data = response.json()["data"]
Disponibilidad
Franjas horarias y plazas libres.
GET /api/v1/products/{id}/slots
curl
curl "https://passavo.eu/api/v1/products/12/slots" \
-H "Authorization: Bearer pv_live_YOUR_API_KEY" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->withToken('pv_live_YOUR_API_KEY')
->get('https://passavo.eu/api/v1/products/12/slots');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/products/12/slots', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer pv_live_YOUR_API_KEY',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/products/12/slots", headers=headers)
data = response.json()["data"]
Ventas
Descuentos, vales regalo y abonos anuales.
GET /api/v1/discounts
curl
curl "https://passavo.eu/api/v1/discounts" \
-H "Authorization: Bearer pv_live_YOUR_API_KEY" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->withToken('pv_live_YOUR_API_KEY')
->get('https://passavo.eu/api/v1/discounts');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/discounts', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer pv_live_YOUR_API_KEY',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/discounts", headers=headers)
data = response.json()["data"]
Pedidos
Crear, pagar, cancelar y reembolsar pedidos.
GET /api/v1/orders
curl
curl "https://passavo.eu/api/v1/orders" \
-H "Authorization: Bearer pv_live_YOUR_API_KEY" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->withToken('pv_live_YOUR_API_KEY')
->get('https://passavo.eu/api/v1/orders');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/orders', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer pv_live_YOUR_API_KEY',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/orders", headers=headers)
data = response.json()["data"]
Entradas
Consultar, anular y reenviar entradas.
GET /api/v1/tickets
curl
curl "https://passavo.eu/api/v1/tickets" \
-H "Authorization: Bearer pv_live_YOUR_API_KEY" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->withToken('pv_live_YOUR_API_KEY')
->get('https://passavo.eu/api/v1/tickets');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/tickets', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer pv_live_YOUR_API_KEY',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/tickets", headers=headers)
data = response.json()["data"]
Acceso
Registro de entrada en la puerta.
GET /api/v1/check-ins
curl
curl "https://passavo.eu/api/v1/check-ins" \
-H "Authorization: Bearer pv_live_YOUR_API_KEY" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->withToken('pv_live_YOUR_API_KEY')
->get('https://passavo.eu/api/v1/check-ins');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/check-ins', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer pv_live_YOUR_API_KEY',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/check-ins", headers=headers)
data = response.json()["data"]
Clientes
Compradores, resumidos a partir de los pedidos.
GET /api/v1/customers
curl
curl "https://passavo.eu/api/v1/customers" \
-H "Authorization: Bearer pv_live_YOUR_API_KEY" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->withToken('pv_live_YOUR_API_KEY')
->get('https://passavo.eu/api/v1/customers');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/customers', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer pv_live_YOUR_API_KEY',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/customers", headers=headers)
data = response.json()["data"]
Meta
La descripción de la propia API.
GET /api/v1/openapi.json
curl
curl "https://passavo.eu/api/v1/openapi.json" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->get('https://passavo.eu/api/v1/openapi.json');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/openapi.json', {
method: 'GET',
headers: {
Accept: 'application/json',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json"}
response = requests.get("https://passavo.eu/api/v1/openapi.json", headers=headers)
data = response.json()["data"]
Webhooks
Mensajes que enviamos a tu servidor.
GET /api/v1/webhook-deliveries
curl
curl "https://passavo.eu/api/v1/webhook-deliveries" \
-H "Authorization: Bearer pv_live_YOUR_API_KEY" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->withToken('pv_live_YOUR_API_KEY')
->get('https://passavo.eu/api/v1/webhook-deliveries');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/webhook-deliveries', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer pv_live_YOUR_API_KEY',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/webhook-deliveries", headers=headers)
data = response.json()["data"]
Webhooks
Un webhook es lo contrario de una llamada: en lugar de preguntar una y otra vez si ya se ha pagado, enviamos un mensaje a una dirección suya en cuanto ocurre. Eso ahorra miles de llamadas al día y usted lo sabe al instante en vez de en la siguiente ronda.
Gestiona sus destinos en su pantalla de gestión en Ajustes → Acceso API, pestaña Webhooks, o mediante la API con una clave que lleva el derecho webhooks. Cada destino tiene su propio secreto; con él comprueba que un mensaje viene de verdad de nosotros.
Comprobar la firma
Cada mensaje lleva la cabecera Passavo-Signature con la forma t=<unix>,v1=<hmac-sha256>. t es el momento en que firmamos; v1 es un HMAC-SHA256 con el secreto de ese destino sobre la marca de tiempo, un punto y el cuerpo en bruto. Calcúlelo usted y compare en tiempo constante — una comparación normal delata, por lo que tarda, cuántos caracteres eran correctos. Rechace también un mensaje cuyo t se aleje más de 300 segundos de su propio reloj: así nadie puede reproducir más tarde un mensaje interceptado.
Compruebe sobre el cuerpo EN BRUTO, antes de que su framework lo convierta en json. Reconstruir el json pone las claves en otro orden y entonces ya no coincide ninguna firma.
curl
# De handtekening narekenen vanaf de opdrachtregel.
# T is de waarde van t= uit de header Passavo-Signature,
# het resultaat hoort gelijk te zijn aan de waarde van v1=.
printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET"
PHP
// De RUWE body, niet $request->all().
$body = $request->getContent();
// "t=1758355200,v1=9f86d0…" uit elkaar halen.
$delen = [];
foreach (explode(',', (string) $request->header('Passavo-Signature')) as $stuk) {
[$sleutel, $waarde] = array_pad(explode('=', trim($stuk), 2), 2, '');
$delen[$sleutel] = $waarde;
}
$t = $delen['t'] ?? '';
$verwacht = hash_hmac('sha256', $t . '.' . $body, $secret);
// hash_equals vergelijkt in constante tijd; de tijdstempel
// houdt een opgevangen bericht tegen dat later opnieuw komt.
if (! ctype_digit($t) || abs(time() - (int) $t) > 300 || ! hash_equals($verwacht, $delen['v1'] ?? '')) {
abort(400);
}
JavaScript
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: de ruwe body als string, bv. via express.raw().
const header = request.headers['passavo-signature'] ?? '';
const delen = Object.fromEntries(header.split(',').map((s) => s.trim().split('=', 2)));
const verwacht = createHmac('sha256', secret).update(delen.t + '.' + rawBody).digest('hex');
const gekregen = delen.v1 ?? '';
const vers = /^\d+$/.test(delen.t ?? '')
&& Math.abs(Date.now() / 1000 - Number(delen.t)) <= 300;
const geldig = vers
&& verwacht.length === gekregen.length
&& timingSafeEqual(Buffer.from(verwacht), Buffer.from(gekregen));
Python
import hashlib, hmac, time
# raw_body: de ruwe body als bytes, bv. request.get_data() in Flask.
header = request.headers.get("Passavo-Signature", "")
delen = dict(stuk.strip().split("=", 1) for stuk in header.split(",") if "=" in stuk)
t = delen.get("t", "")
verwacht = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
# compare_digest vergelijkt in constante tijd.
geldig = (t.isdigit()
and abs(time.time() - int(t)) <= 300
and hmac.compare_digest(verwacht, delen.get("v1", "")))
La lista completa de eventos y de su contenido está en la referencia. →
Qué cambia
Todo cambio que merezca mencionarse recibe una nueva versión con fecha. Si su integración se queda en una fecha antigua, sigue funcionando — la fecha solo dice contra qué comportamiento se construyó.
-
2026-09-20
La primera versión
Claves con derechos, un entorno de pruebas, un único formato de respuesta y paginación por cursor. Lectura: su organización, sedes, productos, franjas horarias, eventos, descuentos, vales regalo y abonos, pedidos, entradas, compradores y escaneos. Escritura: crear, cobrar, anular y reembolsar pedidos, anular entradas, registrar accesos, emitir vales regalo y abonos. Y webhooks salientes, firmados por destino.