Receta: sincronizar ventas con tu CRM
Llevar pedidos y asistentes a tu CRM, tu cuadro de mando o tu contabilidad, sin perder registros ni releerlo todo cada vez.
Última actualización: 23 de agosto de 2026
Llevar lo que pasa en Tickeep a donde ya trabajas: un CRM, un cuadro de mando, una hoja de cálculo, el sistema de tu asesoría.
Lo que necesitas: una clave con orders:read y, si vas a llevarte personas, customers:read. Si además quieres cifras agregadas, reports:read.
Las dos piezas, y por qué hacen falta las dos
Los webhooks te avisan al instante de cada venta. Son lo que necesitas para que una venta aparezca en tu CRM antes de que el comprador cuelgue el teléfono.
La sincronización periódica con updatedSince es la red de seguridad. Recoge lo que se perdiera si tu servidor estuvo caído más de dos horas, o si una suscripción se desactivó sin que te dieras cuenta.
Una integración seria monta las dos. La primera hace el trabajo; la segunda te deja dormir.
La pieza inmediata
export async function POST(req: Request) {
const rawBody = await req.text()
const ok = await verificarFirmaWebhook({
rawBody,
signature: req.headers.get('tickeep-signature'),
secret: process.env.TICKEEP_WEBHOOK_SECRET!,
})
if (!ok) return new Response('firma inválida', { status: 401 })
const evento = JSON.parse(rawBody)
if (!(await db.eventos.insertarSiNoExiste(evento.id))) {
return new Response('ok', { status: 200 })
}
switch (evento.type) {
case 'order.paid': await miCrm.upsertPedido(evento.data.order); break
case 'order.refunded': await miCrm.marcarDevuelto(evento.data.order); break
case 'order.cancelled': await miCrm.marcarCancelado(evento.data.order); break
default: break // habrá eventos nuevos: no falles por ellos
}
return new Response('ok', { status: 200 })
}
Los detalles de firma, reintentos y deduplicación están en webhooks.
La pieza periódica
Cada 15 minutos, cada hora, cada noche: la frecuencia la decides tú.
export async function sincronizarPedidos() {
const marca = await db.leer('tickeep:pedidos:marca') // ISO o undefined
let maxVisto = marca
let cursor: string | undefined
do {
const pagina = await tickeep.orders({
updatedSince: marca,
limit: 100, // el máximo: menos peticiones
cursor,
})
for (const pedido of pagina.data) {
await miCrm.upsertPedido(pedido) // upsert por reference, nunca insert
if (!maxVisto || pedido.updatedAt > maxVisto) maxVisto = pedido.updatedAt
}
cursor = pagina.nextCursor ?? undefined
} while (cursor)
// AL FINAL, y solo si todo fue bien
if (maxVisto) await db.guardar('tickeep:pedidos:marca', maxVisto)
}
Las tres reglas
Guarda la marca al final, no al principio. Si la pasada se rompe a mitad, la siguiente vuelve a empezar desde donde acabó la última completa. Reprocesar un pedido no cuesta nada; saltárselo, sí.
Usa el updatedAt máximo que has visto, no la hora de tu reloj. Tu servidor y el nuestro no tienen por qué coincidir al milisegundo, y una desviación de dos segundos en la dirección mala se come registros en silencio.
Haz tu escritura idempotente. Un upsert por reference, no un insert. Las ventanas se solapan siempre un poco, y volverás a ver cosas que ya tenías. También lo verás por el webhook, así que el mismo pedido te llegará por dos caminos: si tu escritura es idempotente, da igual.
Sin updatedSince te traes el histórico entero. Con limit=100 son 10 peticiones por cada 1.000 pedidos, así que suele entrar de sobra en el límite. Si tienes decenas de miles y te preocupa, avísanos y te subimos el tope de esa clave el rato que dure.
Los asistentes
Mismo patrón, otro listado:
export async function sincronizarPersonas() {
const marca = await db.leer('tickeep:asistentes:marca')
let maxVisto = marca
let cursor: string | undefined
do {
const pagina = await tickeep.attendees({ updatedSince: marca, limit: 100, cursor })
for (const persona of pagina.data) {
await miCrm.upsertContacto({
email: persona.email,
nombre: persona.name,
telefono: persona.phone,
pedidos: persona.orderCount,
gastado: persona.totalSpent.amount / 100,
// OJO: no lo pierdas por el camino
aceptaMarketing: persona.marketingConsent,
})
if (!maxVisto || persona.updatedAt > maxVisto) maxVisto = persona.updatedAt
}
cursor = pagina.nextCursor ?? undefined
} while (cursor)
if (maxVisto) await db.guardar('tickeep:asistentes:marca', maxVisto)
}
marketingConsent dice si esa persona aceptó recibir comunicaciones comerciales. Llévatelo a tu CRM y respétalo. Que alguien te haya comprado una entrada no te autoriza a mandarle publicidad, y la responsabilidad es de quien manda el email — no de Tickeep.
Recuerda que los asistentes son por organización, no por sala: quien compra en dos salas tuyas es una sola persona, con un total gastado que suma las dos. Este listado no acepta venueId.
Cifras agregadas
Si lo que quieres es alimentar un cuadro de mando, no hace falta que sumes tú:
const informe = await tickeep.salesReport({ venueId, preset: '30d' })
// informe.summary.total → recaudación del periodo
// informe.previousSummary.total → el periodo anterior, para la comparativa
// informe.timeline → un punto por día, para la gráfica
// informe.summary.byChannel → el reparto online / taquilla / api, ya hecho
Usa el mismo motor que el informe del panel, así que las cifras coinciden. Si tu cuadro de mando y la pantalla de la sala dieran números distintos, la sala dejaría de creerse los dos. Ver asistentes e informes.
Qué guardar en tu lado
| Guarda | Por qué |
|---|---|
reference del pedido | Es la clave con la que se habla del pedido en todas partes |
updatedAt | Para la marca de sincronización |
| Los importes en céntimos | Convierte solo al pintar. Ver convenciones |
source | Distingue web, taquilla y API |
lines[].eventName | Ya viene congelado del día de la venta |
Si guardas 12,50 en un campo flotante y luego sumas mil filas, el total no cuadrará con el de Tickeep. Guarda 1250 y divide por 100 en el momento de mostrarlo.
Los estados que hay que reflejar
Un pedido no se queda quieto. Si solo escuchas order.paid, tu CRM se queda con ventas que ya se devolvieron:
| Estado | Qué significa en tu CRM |
|---|---|
pending | Reserva en curso. No es una venta todavía |
paid | Venta cerrada |
cancelled | Reserva que no llegó a nada. No cuenta |
failed | Cobro roto. No cuenta |
refunded | Fue una venta y ya no lo es. Réstala |
Los refunded son los que más se olvidan, y son los que descuadran la recaudación al cierre del mes.
Antes de dejarlo en automático
Prueba una pasada completa sin marca
Y comprueba que el número de pedidos cuadra con el panel.
Prueba una pasada incremental
Haz una venta de prueba y comprueba que aparece, y solo una vez.
Rompe la pasada a propósito
Corta el proceso a mitad y comprueba que la siguiente recupera lo que faltaba.
Devuelve un pedido y mira tu CRM
Es el caso que casi nadie prueba y el que descuadra los cierres.
Ponle una alerta al proceso
Si la sincronización lleva dos días fallando, alguien tiene que enterarse antes del cierre del mes.
Seguir por aquí
Cómo recorrer un listado sin saltarte filas y cómo ponerte al día sin releer el catálogo entero cada vez.
El listado de personas de tu organización y el informe de recaudación, con el corte por canal ya calculado.
Que Tickeep avise a tu servidor cuando pasa algo, en vez de preguntar en bucle. Eventos, firma, reintentos y los tres fallos que se cometen siempre.