Si tu negocio depende de reservas, seguramente necesitas que esos datos vivan en más de un sitio. Tu CRM necesita saber cuándo reserva un cliente nuevo. Tu ERP necesita los datos de ingresos. Tu herramienta de marketing necesita disparar un email de bienvenida. Y tu app personalizada puede necesitar crear reservas de forma programática.
La API pública y los webhooks de Cowlendar hacen posible todo esto. La API te permite leer tus servicios y reservas, y crear nuevas reservas desde cualquier sistema externo. Los webhooks envían notificaciones en tiempo real a tu servidor cada vez que pasa algo: se crea, confirma, cancela o reprograma una reserva, o sucede un evento de suscripción.
Ambas funciones están actualmente en Beta.
¿Qué puedes construir con la API de Cowlendar?
Estos son escenarios reales de integración que los merchants de Cowlendar ya están construyendo:
Sincronizar cada reserva con HubSpot, Salesforce o cualquier CRM. Cuando un cliente reserva un corte de pelo, una clase de yoga o un alquiler, un webhook se dispara al instante con todos los datos de la reserva: nombre, email, teléfono, servicio, fecha, precio y miembro del equipo. Tu CRM crea o actualiza el contacto automáticamente, sin carga manual.
Crear tu propia página de reservas o app móvil. Usa la API para obtener tus servicios, incluyendo duraciones, miembros del equipo, equipment y categorías de participantes, y crear reservas desde tu propio frontend. La reserva dispara los mismos emails de confirmación, notificaciones SMS y sincronización con Google Calendar que cualquier reserva hecha desde el widget de Cowlendar.
Automatizar flujos con Zapier o Make sin código. Apunta un webhook de Cowlendar a la URL de webhook de Zapier o Make. Cada evento de reserva se convierte en un trigger. Envía una notificación a Slack cuando alguien reserve, agrega una fila en Google Sheets, crea una tarjeta en Trello o dispara una secuencia de emails en Klaviyo o Mailchimp.
Enviar datos de reservas a tu herramienta de reporting o BI. Usa el endpoint List Bookings para traer todas las reservas dentro de un rango de fechas, filtradas por estado, servicio, asistencia o email del cliente. Exporta esos datos a tu data warehouse, Google Sheets o Airtable para crear dashboards a medida.
Vincular reservas con créditos de suscripción. Cuando creas una reserva vía API, puedes pasar un subscription_id para descontar automáticamente un crédito del plan del cliente, por ejemplo un pase de yoga de 10 clases.
Conectar reservas de POS o del admin con sistemas externos. Los webhooks se disparan para todas las fuentes de reserva: el widget de storefront, el panel de admin, POS y la propia API. Nada se pierde por el camino.
Autenticación
Cada request a la API requiere un Bearer token generado desde tu admin de Cowlendar. Los tokens tienen acceso total de lectura y escritura a las reservas y servicios de tu tienda. Trátalos como contraseñas: no los subas a control de versiones ni los compartas en canales públicos.
Cómo generar tu token de API
Paso 1: En tu admin de Shopify, abre la app Cowlendar.
Paso 2: Ve a Settings > Public API.
Paso 3: Haz clic en Generate token.
Paso 4: Dale a tu token un nombre descriptivo para identificarlo más tarde, por ejemplo "HubSpot sync", "Zapier" o "Mobile app".
Paso 5: Haz clic en Generate. Cowlendar te muestra el token completo solo una vez. Cópialo en ese momento y guárdalo en un lugar seguro, como un gestor de contraseñas o las variables de entorno de tu servidor.
Puedes revocar cualquier token en cualquier momento desde esa misma página. Revocar un token detiene al instante todos los requests que lo estén usando.
Cómo usar tu token
Incluye el token en el header Authorization de cada request:
Authorization: Bearer YOUR_TOKEN_HERE
Ejemplo con curl:
curl https://app.cowlendar.com/public-api/v1/services -H "Authorization: Bearer YOUR_TOKEN_HERE"
Si el token falta o es inválido, la API devuelve 401.
Endpoints de la API
Base URL: https://app.cowlendar.com
Listar servicios
GET /public-api/v1/services
Devuelve todos los servicios no archivados de tu tienda. Usa este endpoint para descubrir tus IDs de servicio, tipos, duraciones, miembros del equipo, equipment y categorías de participantes antes de crear reservas vía API.
Query parameters:
limit (opcional, 1-100): número de resultados por página.
cursor (opcional): cursor de paginación de una respuesta anterior.
is_active (opcional, true o false): filtra por servicios activos o inactivos.
type (opcional): filtra por tipo de servicio. Valores: classic-checkout, classic-no-checkout, multiday-checkout, multiday-no-checkout, multislot-checkout, multislot-no-checkout, fullday-checkout, fullday-no-checkout.
q (opcional, máximo 120 caracteres): busca servicios por título.
sort (opcional): -id por defecto, más nuevos primero, id más antiguos primero, title de A a Z, -title de Z a A.
Ejemplo de request:
curl "https://app.cowlendar.com/public-api/v1/services?is_active=true&limit=10" -H "Authorization: Bearer YOUR_TOKEN_HERE"
Ejemplo de respuesta, resumida:
{
"data": [
{
"id": "6789abcdef0123456789abcd",
"title": "Haircut",
"type": "classic-checkout",
"is_active": true,
"timezone": "Europe/Paris",
"durations": [30, 45, 60],
"default_duration": 30,
"avs_type": "teammates",
"meeting_location": "in_person",
"enable_participants": false,
"participants": [],
"equipments": [],
"teammates": [
{
"id": "aaa111bbb222ccc333ddd444",
"firstname": "Marie",
"lastname": "Dupont",
"email": "[email protected]",
"thumbnail": null
}
],
"product": {
"handle": "haircut",
"title": "Haircut",
"image": "https://cdn.shopify.com/..."
},
"created_at": "2025-09-01T10:00:00.000Z",
"updated_at": "2026-05-10T08:30:00.000Z"
}
],
"pagination": {
"has_more": false,
"next_cursor": null
}
}
Cómo entender los campos clave del servicio
avs_type te dice cómo gestiona la disponibilidad ese servicio y define si necesitas pasar un teammate_id al crear reservas:
avs_type: "teammates" significa que cada miembro del equipo tiene su propio horario independiente. Al crear una reserva, pasa un teammate_id del array teammates. Si no lo envías, se usa como fallback al propietario de la tienda.
avs_type: "service" significa que el propio servicio controla su disponibilidad. El miembro asignado se determina automáticamente según la hora de la reserva. Aun así, puedes pasar un teammate_id para forzar la asignación a una persona concreta.
avs_type: "equipment" significa que la disponibilidad es por equipment. Se parece al modo service, pero ligado a la capacidad del recurso.
enable_participants te indica si el servicio usa categorías tipadas de participantes, como "Adult", "Child" o "Senior". Cuando está en true, el array participants incluye las categorías con sus IDs, cantidades mínimas, máximas y valores por defecto. Necesitas esos IDs para crear reservas con desglose detallado de participantes.
equipments contiene recursos reservables, como salas, canchas, vehículos o bicicletas, con sus variantes y SKUs. Para equipment con tracking_type: "same", todas las unidades son intercambiables y puedes usar cualquier SKU, normalmente base. Para tracking_type: "unique", cada unidad tiene su propio SKU y debes elegir uno de variants[].sku.
type te dice qué modelo de reserva usa el servicio. El sufijo -checkout o -no-checkout indica si el servicio pasa por checkout de Shopify. El prefijo indica el modelo de agenda: classic para un solo horario, multiday para varios días como una estancia, multislot para varios slots consecutivos y fullday para una reserva de día completo.
Listar reservas
GET /public-api/v1/bookings
Devuelve las reservas de tu tienda. Este es el endpoint que usarás para sincronizaciones con CRM, exportaciones de reporting y dashboards personalizados de reservas.
Query parameters:
limit (opcional, 1-100): resultados por página.
cursor (opcional): cursor de paginación.
start (opcional, datetime ISO 8601): solo reservas que empiecen en esa fecha o después.
end (opcional, datetime ISO 8601): solo reservas que empiecen antes de esa fecha.
service_id (opcional, array de IDs): filtra por uno o varios servicios.
subscription_id (opcional, array de IDs): filtra por suscripciones.
status (opcional, array): confirmed, pending, declined, canceled.
attendance (opcional, array): booked, pending, arrived, started, completed, no-show, delayed, paid.
customer_email (opcional): filtra por email exacto del cliente.
sort (opcional): -id por defecto, más nuevos primero, id más antiguos primero, start_date primero los más próximos, -start_date primero los más recientes.
Ejemplo: obtener todas las reservas confirmadas de la próxima semana:
curl "https://app.cowlendar.com/public-api/v1/bookings?status=confirmed&start=2026-05-25T00:00:00Z&end=2026-06-01T00:00:00Z&sort=start_date" -H "Authorization: Bearer YOUR_TOKEN_HERE"
Ejemplo: encontrar todas las reservas de un cliente específico:
curl "https://app.cowlendar.com/public-api/v1/[email protected]" -H "Authorization: Bearer YOUR_TOKEN_HERE"
Ejemplo: exportar todos los no-shows de un servicio concreto:
curl "https://app.cowlendar.com/public-api/v1/bookings?attendance=no-show&service_id=6789abcdef0123456789abcd" -H "Authorization: Bearer YOUR_TOKEN_HERE"
Ejemplo de respuesta, resumida:
{
"data": [
{
"id": "abc123def456ghi789jkl012",
"booking_str": "COW-1234",
"service": {
"id": "6789abcdef0123456789abcd",
"title": "Haircut",
"type": "classic-checkout"
},
"start_date": "2026-05-27T14:00:00.000Z",
"end_date": "2026-05-27T14:30:00.000Z",
"timezone": "Europe/Paris",
"customer": {
"name": "Jane Smith",
"email": "[email protected]",
"phone": "+33612345678",
"locale": "en"
},
"form_data": {
"Special requests": "Window seat"
},
"quantity": 1,
"unit_quantity": 1,
"quantity_details": [],
"price": { "amount": 35, "currency": "EUR" },
"confirmation_status": "confirmed",
"attendance": "booked",
"financial_status": "paid",
"is_canceled": false,
"teammates": [
{
"id": "aaa111bbb222ccc333ddd444",
"firstname": "Marie",
"lastname": "Dupont"
}
],
"order_id": "gid://shopify/Order/123456789",
"subscription_id": null,
"created_at": "2026-05-20T09:15:00.000Z",
"updated_at": "2026-05-20T09:15:00.000Z"
}
],
"pagination": {
"has_more": false,
"next_cursor": null
}
}
Cómo entender los campos clave de la reserva
quantity y unit_quantity funcionan juntos. En un servicio classic, como un corte de pelo, quantity es el número de clientes asistentes y unit_quantity es 1. En un servicio multiday, como una estancia, quantity es el número de habitaciones y unit_quantity es el número de noches. En un servicio multislot, quantity siempre es 1 y unit_quantity es el número de slots reservados. Las unidades totales facturables siempre son quantity x unit_quantity.
quantity_details es el desglose detallado. Puede contener tres tipos de entradas: default para un conteo anónimo, participant para un participante tipado como "Adult" o "Child", y equipment para un recurso reservable como "Court 3". Las reservas antiguas pueden tener este array vacío.
confirmation_status puede ser confirmed, pending o declined. Algunos servicios requieren confirmación manual del admin antes de que la reserva quede activa.
attendance sigue el ciclo de asistencia del cliente: booked, pending, arrived, started, completed, no-show, delayed, paid.
order_id es el ID de la orden de Shopify cuando la reserva pasó por checkout. Es null para reservas manuales y reservas creadas vía API.
subscription_id vincula la reserva a un plan de suscripción, por ejemplo un pase de 10 clases.
Crear una reserva vía API
POST /public-api/v1/bookings
Crea una reserva de forma programática. Se trata como una manual booking, lo que significa que no se crea ninguna orden de Shopify. Aun así, las automatizaciones normales siguen funcionando: emails de confirmación al cliente, emails y SMS al equipo y sincronización de calendario con Google Calendar u Outlook.
Este endpoint es el que usarás si estás construyendo un frontend de reservas propio, una app móvil, un kiosk o si estás importando reservas desde otro sistema.
Campos obligatorios:
service_id (string): el ID del servicio, que obtienes desde List Services.
start_date (datetime ISO 8601): cuándo empieza la reserva.
end_date (datetime ISO 8601): cuándo termina la reserva.
timezone (string): zona horaria IANA, por ejemplo Europe/Paris, America/New_York, Asia/Tokyo.
customer (object): debe incluir al menos email o phone. También puede incluir name, firstname, lastname y locale, que es el código de idioma para los emails del cliente.
Campos opcionales:
quantity (integer, default 1): unidades principales reservadas. Se ignora si envías quantity_details.
unit_quantity (integer, default 1): subunidades por unidad principal.
quantity_details (array): desglose detallado por tipo de participante o equipment. Cuando lo envías, quantity se calcula automáticamente como la suma.
teammate_id (string): fuerza la asignación a un miembro concreto del equipo.
subscription_id (string): vincula esta reserva a una suscripción existente y descuenta un crédito.
form_data (object): pares clave o valor personalizados de tu formulario de reserva.
price (number, default 0): precio total neto de la reserva.
currency (string): código ISO 4217 como USD, EUR o GBP. Por defecto usa la moneda de tu tienda.
Ejemplo: crear una reserva simple
Un cliente reserva un corte de pelo de 30 minutos con un estilista específico:
curl -X POST https://app.cowlendar.com/public-api/v1/bookings -H "Authorization: Bearer YOUR_TOKEN_HERE" -H "Content-Type: application/json" -d '{
"service_id": "6789abcdef0123456789abcd",
"start_date": "2026-06-01T10:00:00.000Z",
"end_date": "2026-06-01T10:30:00.000Z",
"timezone": "Europe/Paris",
"customer": {
"email": "[email protected]",
"name": "John Doe",
"phone": "+33612345678",
"locale": "en"
},
"teammate_id": "aaa111bbb222ccc333ddd444",
"quantity": 1,
"price": 35,
"currency": "EUR"
}'
Cowlendar crea la reserva y dispara el email de confirmación, el SMS y la invitación de calendario tanto para el cliente como para el miembro del equipo.
Ejemplo: reserva grupal con participantes tipados
Una familia reserva una visita guiada: 3 adultos y 2 niños con precios distintos:
curl -X POST https://app.cowlendar.com/public-api/v1/bookings -H "Authorization: Bearer YOUR_TOKEN_HERE" -H "Content-Type: application/json" -d '{
"service_id": "6789abcdef0123456789abcd",
"start_date": "2026-06-15T09:00:00.000Z",
"end_date": "2026-06-15T11:00:00.000Z",
"timezone": "America/New_York",
"customer": {
"email": "[email protected]",
"firstname": "Sarah",
"lastname": "Miller",
"phone": "+15551234567",
"locale": "en"
},
"quantity_details": [
{
"type": "participant",
"name": "Adult",
"quantity": 3,
"participant_id": "adult_id_from_service"
},
{
"type": "participant",
"name": "Child (under 12)",
"quantity": 2,
"participant_id": "child_id_from_service"
}
],
"price": 175,
"currency": "USD"
}'
Los valores participant_id salen del array participants[] del servicio en la respuesta de List Services. El total de quantity se fija automáticamente en 5, 3 + 2.
Ejemplo: reserva con equipment
Un cliente reserva una cancha de tenis durante 1 hora:
curl -X POST https://app.cowlendar.com/public-api/v1/bookings -H "Authorization: Bearer YOUR_TOKEN_HERE" -H "Content-Type: application/json" -d '{
"service_id": "6789abcdef0123456789abcd",
"start_date": "2026-06-10T16:00:00.000Z",
"end_date": "2026-06-10T17:00:00.000Z",
"timezone": "Europe/London",
"customer": {
"email": "[email protected]",
"name": "Tom Wilson"
},
"quantity_details": [
{
"type": "equipment",
"name": "Court 3",
"quantity": 1,
"equipment_id": "eee555fff666ggg777hhh888",
"equipment_sku": "COURT3",
"capacity": 4
}
],
"price": 25,
"currency": "GBP"
}'
Los valores equipment_id y equipment_sku salen del array equipments[] del servicio. Para equipment con tracking_type: "same", usa cualquier SKU, normalmente base. Para tracking_type: "unique", usa el variants[].sku específico de la unidad que quieres reservar.
Ejemplo: reserva vinculada a suscripción
Un miembro de un estudio de yoga usa un crédito de su pase mensual de 10 clases:
curl -X POST https://app.cowlendar.com/public-api/v1/bookings -H "Authorization: Bearer YOUR_TOKEN_HERE" -H "Content-Type: application/json" -d '{
"service_id": "6789abcdef0123456789abcd",
"start_date": "2026-06-05T08:00:00.000Z",
"end_date": "2026-06-05T09:00:00.000Z",
"timezone": "America/Los_Angeles",
"customer": {
"email": "[email protected]",
"name": "Lisa Chen",
"locale": "en"
},
"subscription_id": "fff000eee111ddd222ccc333",
"price": 0
}'
La suscripción debe estar active, pertenecer al mismo servicio y tener remaining_credits > 0. Cowlendar descuenta automáticamente el crédito y envía al cliente el email de actualización de suscripción.
Códigos de error
400 Error de validación. Falta un campo obligatorio o algún valor tiene un formato incorrecto. El body del error te indica qué campo es.
401 Falta el token o el token es inválido.
403 La tienda está inactiva o el acceso está restringido.
404 Servicio no encontrado.
422 Reserva rechazada. El horario ya está tomado, el miembro del equipo no está disponible, el equipment llegó a su capacidad o la suscripción no tiene créditos restantes. Revisa el mensaje de error para ver la causa exacta.
429 Se excedió el rate limit. Espacia tus requests y vuelve a intentarlo unos momentos después.
Paginación
Todos los endpoints de lista usan paginación por cursor. Cuando la respuesta incluye "has_more": true, pasa el valor de next_cursor como parámetro cursor en el siguiente request:
curl "https://app.cowlendar.com/public-api/v1/bookings?cursor=eyJpZCI6IjY3ODlhYmNkIn0=&limit=50" -H "Authorization: Bearer YOUR_TOKEN_HERE"
El orden de clasificación se mantiene entre páginas. El tamaño máximo por página es de 100 resultados.
Webhooks: notificaciones de reservas en tiempo real
Mientras la API te permite traer datos bajo demanda, los webhooks te empujan los datos al instante. Cada vez que se crea, confirma, cancela o reprograma una reserva, Cowlendar envía un HTTP POST firmado a la URL que configures.
Los webhooks son clave para integraciones en tiempo real: sincronizaciones con CRM, notificaciones a Slack, secuencias automáticas de emails, dashboards live o cualquier flujo que necesite reaccionar a eventos de reserva en cuanto ocurren.
Configura un endpoint de webhook
Paso 1: En tu admin de Shopify, abre la app Cowlendar.
Paso 2: Ve a Settings > Webhooks.
Paso 3: Haz clic en Add endpoint.
Paso 4: Escribe un nombre para tu endpoint, por ejemplo "HubSpot CRM", "Zapier" o "Analytics pipeline".
Paso 5: Pega tu HTTPS URL. La URL debe usar HTTPS. Si usas Zapier o Make, pega la URL de webhook que ellos te entregan.
Paso 6: Marca los eventos que quieres recibir. Puedes elegir cualquier combinación.
Paso 7: Haz clic en Create.
Cowlendar te mostrará un signing secret que empieza por whsec_. Cópialo al instante y guárdalo de forma segura. Ese secret solo se muestra una vez y lo necesitarás para verificar los requests entrantes en tu servidor.
Más adelante puedes editar cualquier endpoint para cambiar la URL o activar y desactivar eventos concretos:
Los 8 eventos de webhook
booking.created se dispara cuando se crea una nueva reserva desde cualquier origen: el widget de storefront, el panel de admin, POS o la Public API.
booking.confirmed se dispara cuando el admin confirma manualmente una reserva pendiente.
booking.declined se dispara cuando se rechaza una reserva pendiente.
booking.canceled se dispara cuando el cliente o el admin cancela una reserva.
booking.rescheduled se dispara cuando cambia la fecha de una reserva o los miembros del equipo asignados.
booking.attendance_changed se dispara cuando cambia el estado de asistencia, por ejemplo de "booked" a "completed" o cuando se marca como "no-show".
subscription.created se dispara cuando se crea una nueva suscripción recurrente.
subscription.billing_failed se dispara cuando falla un intento de cobro de una suscripción. Después de 3 fallos consecutivos, el contrato de suscripción se cancela automáticamente en Shopify y el estado local cambia a canceled.
Cómo se ve el payload de un webhook
Cada entrega de webhook llega como un HTTP POST con un body JSON. Aquí tienes un ejemplo completo de booking.created:
{
"id": "evt_2b86f80a-1c3d-4e5f-9a8b-7c6d5e4f3a2b",
"type": "booking.created",
"created_at": "2026-05-20T09:15:00.000Z",
"shop_domain": "yourshop.myshopify.com",
"data": {
"id": "abc123def456ghi789jkl012",
"booking_str": "COW-1234",
"service": {
"id": "6789abcdef0123456789abcd",
"title": "Haircut",
"type": "classic-checkout"
},
"start_date": "2026-05-27T14:00:00.000Z",
"end_date": "2026-05-27T14:30:00.000Z",
"timezone": "Europe/Paris",
"customer": {
"name": "Jane Smith",
"email": "[email protected]",
"phone": "+33612345678",
"locale": "en"
},
"form_data": {},
"quantity": 1,
"unit_quantity": 1,
"quantity_details": [],
"price": { "amount": 35, "currency": "EUR" },
"confirmation_status": "confirmed",
"attendance": "booked",
"financial_status": null,
"is_canceled": false,
"teammates": [
{
"id": "aaa111bbb222ccc333ddd444",
"firstname": "Marie",
"lastname": "Dupont"
}
],
"order_id": null,
"subscription_id": null,
"created_at": "2026-05-20T09:15:00.000Z",
"updated_at": "2026-05-20T09:15:00.000Z"
}
}
El campo data contiene el objeto completo de reserva o suscripción con exactamente la misma estructura que la respuesta de la API.
En eventos subscription.billing_failed, el payload incluye además un objeto previous con el failure_count antes del intento actual, para que puedas seguir la escalada, por ejemplo enviando un email urgente tras el segundo fallo.
Headers que llegan en cada entrega de webhook
X-Cowlendar-Event-Id es el ID único del evento. Cowlendar reutiliza el mismo ID cuando reintenta una entrega fallida. Úsalo para deduplicar de tu lado.
X-Cowlendar-Event-Type es el string del tipo de evento, por ejemplo booking.created.
X-Cowlendar-Timestamp es el timestamp Unix en segundos en el que Cowlendar firmó el request.
X-Cowlendar-Signature es la firma HMAC-SHA256 con formato t=<timestamp>,v1=<hex>.
Verifica la firma del webhook
Cada request de webhook va firmado para que puedas confirmar que realmente viene de Cowlendar y que nadie lo alteró. La firma se calcula como HMAC-SHA256(secret, timestamp + "." + raw_body) usando el signing secret de tu endpoint.
También deberías rechazar requests con un timestamp de más de 5 minutos para evitar replay attacks.
Node.js (Express):
import crypto from "crypto";
import express from "express";
const app = express();
app.post(
"/webhooks/cowlendar",
express.raw({ type: "application/json" }),
(req, res) => {
const sigHeader =
req.header("X-Cowlendar-Signature") || "";
const timestamp =
req.header("X-Cowlendar-Timestamp") || "";
const rawBody = req.body.toString("utf8");
// Reject requests older than 5 minutes
if (
Math.abs(Date.now() / 1000 - parseInt(timestamp, 10))
> 300
) {
return res.status(400).send("Timestamp too old");
}
const expected = crypto
.createHmac(
"sha256",
process.env.COWLENDAR_WEBHOOK_SECRET
)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const received = sigHeader
.split(",")
.find((p) => p.startsWith("v1="))
?.slice(3);
if (
!received ||
!crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(received)
)
) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(rawBody);
// Short-circuit test pings
if (event.type === "webhook.ping") {
return res.status(200).send("pong");
}
// Handle real events here
res.status(200).send("ok");
}
);
Python (Flask):
import hmac, hashlib, os, time, json
from flask import Flask, request
app = Flask(__name__)
@app.post("/webhooks/cowlendar")
def cowlendar_webhook():
sig_header = request.headers.get(
"X-Cowlendar-Signature", ""
)
timestamp = request.headers.get(
"X-Cowlendar-Timestamp", ""
)
raw_body = request.get_data(as_text=True)
# Reject requests older than 5 minutes
if abs(time.time() - int(timestamp)) > 300:
return "Timestamp too old", 400
expected = hmac.new(
os.environ["COWLENDAR_WEBHOOK_SECRET"].encode(),
f"{timestamp}.{raw_body}".encode(),
hashlib.sha256,
).hexdigest()
received = next(
(p[3:] for p in sig_header.split(",")
if p.startswith("v1=")),
None,
)
if not received or not hmac.compare_digest(
expected, received
):
return "Invalid signature", 401
event = json.loads(raw_body)
if event["type"] == "webhook.ping":
return "pong", 200
# Handle real events here
return "ok", 200
PHP:
<?php
$secret = getenv("COWLENDAR_WEBHOOK_SECRET");
$rawBody = file_get_contents("php://input");
$sigHeader = $_SERVER["HTTP_X_COWLENDAR_SIGNATURE"] ?? "";
$timestamp = $_SERVER["HTTP_X_COWLENDAR_TIMESTAMP"] ?? "";
// Reject requests older than 5 minutes
if (abs(time() - (int)$timestamp) > 300) {
http_response_code(400);
exit("Timestamp too old");
}
$expected = hash_hmac(
"sha256",
$timestamp . "." . $rawBody,
$secret
);
$received = null;
foreach (explode(",", $sigHeader) as $part) {
if (str_starts_with($part, "v1=")) {
$received = substr($part, 3);
break;
}
}
if (!$received || !hash_equals($expected, $received)) {
http_response_code(401);
exit("Invalid signature");
}
$event = json_decode($rawBody, true);
if ($event["type"] === "webhook.ping") {
http_response_code(200);
exit("pong");
}
// Handle real events here
http_response_code(200);
echo "ok";
Prueba tu webhook antes de ir a producción
Cada endpoint de webhook tiene un botón Test en el admin. Al hacer clic, envía un evento sintético webhook.ping a tu URL. Ese evento usa la misma firma, los mismos headers y el mismo comportamiento de reintento que los eventos reales, pero con un payload de prueba inofensivo:
{
"id": "evt_2b86f80a...",
"type": "webhook.ping",
"created_at": "2026-05-13T14:57:57.704Z",
"shop_domain": "yourshop.myshopify.com",
"data": {
"message": "This is a test event from Cowlendar. If you can read this, your endpoint is reachable."
}
}
Úsalo para confirmar que Cowlendar puede alcanzar tu URL, que DNS, HTTPS y firewall funcionan bien, y para validar tu lógica de verificación HMAC sin crear una reserva real. Una reserva real enviaría emails al cliente, crearía eventos de calendario y podría incluso disparar una orden de Shopify.
Después de hacer clic en Test, revisa el botón See deliveries para ver el código HTTP que devolvió tu endpoint. Un 200 en verde significa que todo está funcionando.
Reintentos y desactivación automática
Si tu endpoint devuelve un error 5xx, un 429 o sufre un timeout de red, Cowlendar reintenta con este calendario:
Después de 1 minuto, después de 5 minutos, después de 25 minutos, después de 2 horas, después de 10 horas y después de 50 horas. Son 6 intentos en total repartidos a lo largo de unos 3 días.
Después de aproximadamente 20 fallos consecutivos entre todos los eventos, el endpoint se desactiva automáticamente y Cowlendar envía un email al propietario de la tienda. Puedes volver a activarlo desde Settings > Webhooks cuando tu servidor vuelva a estar online.
El mismo X-Cowlendar-Event-Id se reutiliza en los reintentos. Guarda siempre los IDs procesados y omite duplicados.
Cualquier respuesta 2xx cuenta como éxito. No hace falta devolver un body ni un content type específico.
Buenas prácticas para webhooks
Responde dentro de 10 segundos. Si tu handler necesita hacer trabajo pesado, como llamar a otra API, escribir en una base de datos o ejecutar una operación compleja, hazlo de forma asíncrona. Acepta el webhook enseguida con un 200 y luego procésalo en un background job o una cola.
Deduplica usando el event ID. Guarda cada X-Cowlendar-Event-Id que proceses. Si recibes el mismo ID otra vez, sáltalo. Cowlendar reintenta con el mismo ID cuando hay fallos.
Usa comparación en tiempo constante para las firmas. crypto.timingSafeEqual en Node.js, hmac.compare_digest en Python y hash_equals en PHP. Comparaciones de strings normales como === son vulnerables a timing attacks.
No reintentes desde tu lado. Cowlendar ya gestiona los reintentos automáticamente. Si tu servidor tuvo un problema temporal, devuelve 5xx y Cowlendar volverá según su propio calendario.
Maneja webhook.ping de forma explícita. Devuelve 200 inmediatamente para eventos ping. Así puedes usar el botón Test sin disparar tu lógica de negocio.
Limitaciones actuales
Las reservas creadas vía API son manual bookings. No crean una orden de Shopify. Si tu servicio normalmente usa checkout de Shopify, la API lo evita. Los emails de confirmación, SMS y sincronización de calendario siguen funcionando con normalidad.
Todavía no existe un endpoint de disponibilidad. La API actual te permite leer servicios y reservas, y crear reservas. No expone horarios disponibles. Si quieres comprobar si un horario sigue libre, intenta crear la reserva y maneja la respuesta 422 si el slot ya está tomado. Es posible que más adelante se añada un endpoint de disponibilidad.
Todavía no hay endpoints para actualizar o borrar. Puedes crear y leer reservas, pero no puedes actualizarlas, reprogramarlas ni cancelarlas vía API. Por ahora, esas acciones se gestionan desde el admin de Cowlendar.
Se aplican rate limits. Si envías demasiados requests en una ventana corta, la API devuelve 429. Para operaciones masivas, como exportar todas las reservas, añade un pequeño delay entre requests paginados, por ejemplo entre 200 ms y 500 ms.
Los webhook secrets se muestran una sola vez. Si pierdes tu signing secret, elimina el endpoint y crea uno nuevo para obtener un secret fresco.
Los webhooks requieren HTTPS. No se aceptan endpoints HTTP.
FAQ
¿Puedo usar la API de Cowlendar con Zapier o Make sin escribir código?
Sí. Para webhooks, es decir, recibir eventos desde Cowlendar, crea un trigger de "Custom Webhook" en Zapier o Make, copia la URL que te den y añádela como endpoint en Cowlendar Settings > Webhooks. Cada evento de reserva disparará tu automatización. Para la API, o sea enviar requests a Cowlendar, usa la acción "HTTP Request" en Zapier o Make para llamar a cualquier endpoint de Cowlendar con tu Bearer token.
¿Las reservas creadas vía API envían emails a los clientes?
Sí. Las reservas creadas por API se tratan exactamente igual que las reservas manuales. Los emails de confirmación, las notificaciones SMS y la sincronización de calendario se disparan automáticamente, igual que en una reserva hecha desde el panel de admin.
¿Qué pasa si mi endpoint de webhook se cae?
Cowlendar reintenta entregas fallidas 6 veces durante aproximadamente 3 días. Después de unos 20 fallos consecutivos, el endpoint se desactiva automáticamente y recibes un email. Vuelve a activarlo desde Settings > Webhooks cuando tu servidor vuelva a estar estable.
¿Puedo tener varios endpoints de webhook?
Sí. Puedes crear todos los que necesites, cada uno con distintas selecciones de eventos y distintas URLs. Por ejemplo, un endpoint para tu CRM escuchando booking.created y booking.canceled, y otro para tu pipeline de analytics escuchando todos los eventos.
¿Puedo revocar un token de API?
Sí. Ve a Settings > Public API y haz clic en el botón Revoke. El token deja de funcionar de inmediato.
¿Existe sandbox o staging?
No por ahora. Usa el botón Test de webhook.ping para validar tu configuración de webhook sin crear reservas reales. Para pruebas de API, te recomendamos crear un servicio de test en tu admin de Cowlendar y usarlo solo para desarrollo.
¿Dónde está la referencia completa de la API?
La documentación interactiva de la API, con todos los schemas, parámetros y ejemplos de respuesta, está en https://app.cowlendar.com/public-api/docs