Saltar al contenido
Tickeep
Índice de la API

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

Webhooks para lo inmediato, updatedSince para no perder nada

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.

La primera pasada, sin marca

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 viaja con la persona, siempre

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

GuardaPor qué
reference del pedidoEs la clave con la que se habla del pedido en todas partes
updatedAtPara la marca de sincronización
Los importes en céntimosConvierte solo al pintar. Ver convenciones
sourceDistingue web, taquilla y API
lines[].eventNameYa viene congelado del día de la venta
No guardes los importes en decimales

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:

EstadoQué significa en tu CRM
pendingReserva en curso. No es una venta todavía
paidVenta cerrada
cancelledReserva que no llegó a nada. No cuenta
failedCobro roto. No cuenta
refundedFue 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.