Ir al contenido
Passavo
Menú

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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ó.

  1. 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.