Usa la dirección de API de tu organización y una credencial con permisos limitados. Empieza con una solicitud de lectura, revisa la respuesta y mantén los secretos fuera del control de versiones y de los ejemplos de documentación.
Quire tiene una API pública: REST sobre HTTPS, descrita en un documento OpenAPI 3.1, con webhooks firmados para eventos y un servidor MCP para asistentes de IA. La referencia de la API enumera todos los endpoints y eventos.
Direcciones
Cada organización tiene su propia dirección y la API está disponible en ella:
https://acme.quirelms.com/api/v1/coursesLa credencial determina la organización. Se rechaza una clave de una organización si se usa en la dirección de otra.
El documento OpenAPI está disponible en /api/v1/openapi.json desde cualquier dirección de una organización, así que los generadores de clientes siempre ven la versión a la que llamas.
Autenticación
Claves de API: para scripts e integraciones entre servidores. Un administrador crea una en /admin/integrations/api-keys, elige sus permisos y solo puede verla una vez. Envíala como token bearer:
curl -H "Authorization: Bearer qk_live_..." https://acme.quirelms.com/api/v1/users?limit=50Las claves empiezan con qk_live_ o qk_test_. Usa una clave distinta para cada integración.
OAuth 2.1: para apps que actúan como una persona con sesión iniciada. Registra un cliente en /admin/integrations/oauth-clients y luego usa el flujo de código de autorización con PKCE (/oauth/authorize, /oauth/token) o las credenciales de cliente para un cliente de máquina. La detección está en /.well-known/oauth-authorization-server. Un permiso limita lo que puede hacer un token; nunca le permite hacer más de lo que podría hacer la persona.
Los permisos son resource:read, resource:write y resource:delete; por ejemplo, courses:read o enrolments:write. Cuatro son privilegiados y se muestran con una advertencia en la pantalla de consentimiento: audit:read, roles:write, tenants:write y users:delete.

Solicitudes
- Paginación: todas las listas se paginan con cursores. Envía
limity luego pasanext_cursordepagecomocursormientrashas_moresea true (como en el ejemplo). No se usa offset. - Cambios desde una fecha:
updated_sincedevuelve los cambios posteriores a una hora. Combínalo coninclude_deleted=trueo lee/<resource>/deletionspara saber qué se eliminó. - Identificadores externos: la mayoría de los recursos aceptan tu propio
external_id, y/<resource>/ext:{external_id}permite leer o insertar/actualizar mediante ese valor, para que una sincronización no tenga que almacenar los identificadores de Quire. - Idempotencia: envía la cabecera
Idempotency-Keyen solicitudesPOST,PATCHyDELETE. Un reintento con la misma clave devuelve la primera respuesta, en lugar de ejecutar el trabajo dos veces. Los endpoints masivos la requieren. - Versiones: la versión principal está en la ruta (
/v1). Dentro de esa versión, cada cambio incompatible tiene una revisión fechada, que se elige con la cabeceraQuire-Version, por ejemplo,Quire-Version: 2026-09-20. Si no envías la cabecera, recibes la revisión vigente cuando se emitió tu credencial.
Una página de una lista:
{"data": [...], "page": {"next_cursor": "eyJ2Ijox...", "has_more": true, "limit": 100}}Errores
Todos los errores son documentos de problema RFC 9457:
{"type": "https://quire.com/errors/enrolment.seat_limit_reached",
"title": "Seat limit reached", "status": 409,
"code": "enrolment.seat_limit_reached", "category": "conflict",
"detail": "The course has no seats left, so this enrolment was not created. ...",
"request_id": "01JB7XQK4Z..."}Usa code, que es estable, para decidir cómo responder; detail está redactado para las personas, es seguro mostrarlo y puede cambiar. Si no reconoces un código, clasifica el error según category:
| Categoría | Estado | Reintento |
|---|---|---|
validation |
422, con detalles de campos en errors |
No |
authentication |
401 | No |
authorization |
403 | No |
not_found |
404 | No |
conflict |
409 | A veces |
precondition |
412 | No |
quota |
402 para el plan, 413 para el tamaño | No |
rate_limit |
429, con Retry-After |
Sí |
upstream |
502 o 504 | Sí |
internal |
500 | Sí |
Menciona request_id cuando te comuniques con soporte.
Webhooks
Suscríbete en /admin/webhooks o mediante la API en /webhook_subscriptions. Elige eventos por nombre (enrolment.created), por área (enrolment.*) o todos (*). Quire envía primero un webhook.ping; la suscripción comienza cuando tu endpoint responde.
Las entregas siguen la especificación Standard Webhooks:
POST /hooks/quire
webhook-id: 01JB7XQK4Z8FQ2M3N4P5R6S7T8
webhook-timestamp: 1790000000
webhook-signature: v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=Para verificar una entrega:
- Construye la cadena
{webhook-id}.{webhook-timestamp}.{raw body}a partir de los bytes exactos que recibiste, antes de analizar el JSON. - Calcula HMAC-SHA256 sobre la cadena con el secreto de tu suscripción y codifica el resultado en base64.
- Compara en tiempo constante con cada valor
v1,dewebhook-signature. Puede haber dos durante la rotación de un secreto; basta con que uno coincida. - Rechaza una marca de tiempo que difiera más de cinco minutos de tu reloj.
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(secret, id, timestamp, rawBody, header) {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = createHmac('sha256', Buffer.from(secret.replace(/^whsec_/, ''), 'base64'))
.update(`${id}.${timestamp}.${rawBody}`).digest();
return header.split(' ').some((part) => {
const [version, value] = part.split(',');
const given = Buffer.from(value ?? '', 'base64');
return version === 'v1' && given.length === expected.length && timingSafeEqual(given, expected);
});
}Evita entregas duplicadas con webhook-id: un evento puede llegar más de una vez. El cuerpo contiene identificadores y un breve resumen; consulta el recurso para obtener su estado actual. Los reintentos de entregas fallidas se espacian progresivamente durante un máximo de 72 horas; puedes volver a enviarlas desde el registro de entregas.
MCP
El servidor MCP de Quire está en /mcp, en la dirección de la organización, y usa HTTP transmisible. Un cliente MCP obtiene el servidor OAuth de /.well-known/oauth-protected-resource; la persona inicia sesión y da su consentimiento igual que con cualquier cliente OAuth. Las herramientas actúan con los permisos de esa persona, y las operaciones destructivas piden confirmación. Los administradores eligen qué herramientas están disponibles en /admin/integrations/mcp.

Planes y API
Las claves de API, los clientes OAuth, los webhooks y el servidor MCP pertenecen al permiso de API del plan, incluido en todos los planes estándar. Si un plan no lo incluye, se rechazan la creación de claves, clientes y suscripciones, las escrituras REST y las conexiones MCP. Las lecturas REST siguen disponibles para que se puedan exportar los datos. El rechazo es un documento de problema con el código commerce.plan_entitlement, en la categoría precondition.
Extensiones
Los tipos de actividad, bloques, métodos de inscripción y de inicio de sesión, tipos de preguntas, informes, temas e integraciones propios de Quire se declaran en el mismo registro de extensiones que puede ampliar una instalación autoalojada. Las extensiones se compilan en la aplicación: no hay un cargador de complementos en tiempo de ejecución y una organización alojada no puede agregar uno. Los administradores activan o desactivan cada extensión para su organización en /admin/extensions (consulta la guía para administradores).
Para crear una, empieza con el bloque y el tema de ejemplo en packages/integration/extensions/src/sample.ts. Elige el punto de extensión y lee su contrato en points.ts; luego declara la extensión con un ID, una versión, una licencia, lo que ofrece y necesita, y si una organización puede desactivarla. Regístrala donde se integran la aplicación web y el worker para que ambos estén de acuerdo. El registro verifica las reglas de cada punto al crearse y cada vez que llamas a register; rechaza un conjunto no válido e indica todos los problemas, y lo deja sin cambios. Las pruebas de la extensión deben comprobar que extensionContractProblems no detecta problemas en ella y que desactivarla cambia lo que afecta.