@datamo/sdk
v0.4.1
Published
SDK para integrar tu aplicación con Datamo — pull de métricas, webhooks y push en tiempo real
Maintainers
Readme
@datamo/sdk
SDK oficial de Datamo para integrar tu aplicación SaaS.
Datamo es una plataforma de inteligencia unificada que permite a los usuarios ver métricas de todas sus herramientas en un solo lugar. Con este SDK, tu SaaS puede exponer métricas hacia Datamo mediante dos mecanismos complementarios:
- Pull: Datamo sondea tu API periódicamente para obtener métricas actualizadas
- Push: Tu app envía métricas a Datamo en tiempo real cuando ocurre un evento
Instalación
npm install @datamo/sdkRequiere Node.js ≥ 18. createDatamoRouter necesita Express ≥ 4.18 (peer dependency opcional, no se instala automáticamente).
Funciona con require (CommonJS) y con import (ESM) desde 0.4.0:
import { DatamoPushClient } from '@datamo/sdk' // ESM
const { DatamoPushClient } = require('@datamo/sdk') // CommonJS¿Vienes de 0.2.x? Lee la guía de migración a 0.4.1:
^0.2.0no sube solo a 0.4 (es 0.x), y desde el 2026-11-29 Datamo deja de aceptar el push con la firma antigua.
Aviso (0.4.0): la firma legacy del push (
X-Datamo-Signature, con elwebhookTokencomo clave) dejará de aceptarse el 2026-11-29. Pasa tuclientSecretaDatamoPushClientpara firmar también conX-Datamo-Signature-V2. Ver Firma V2 del push.
Cómo funciona
Registro de la integración
Antes de usar el SDK, el desarrollador debe registrar su aplicación en el portal de desarrolladores de Datamo (datamo.app/developer). Al registrar:
- Se genera un
clientIdyclientSecret. ElclientSecretse pasa al SDK (opts.clientSecret) para verificar que las notificaciones de conexión vienen de Datamo - Se configura la
apiBaseUrl: URL base de tu API donde Datamo hará las llamadas - Se define el
credentialSchema: qué datos pedirle al usuario al conectar (API key, usuario/contraseña, etc.) - Se establece el
syncIntervalMinutes: cada cuántos minutos Datamo hace el pull (mínimo 5)
Flujo de conexión
Cuando un usuario de Datamo activa tu integración desde el marketplace:
Usuario activa integración en Datamo
│
▼
Datamo cifra las credenciales del usuario
│
▼
Datamo genera un webhookToken único para esta conexión
│
├──▶ POST {apiBaseUrl}/datamo/connect
│ Body: { workspaceId, webhookUrl, webhookToken, scopes }
│ ← Tu app guarda webhookUrl y webhookToken por workspaceId
│
└──▶ Datamo comienza a sondear GET {apiBaseUrl}/datamo/metrics
Headers: X-Datamo-Workspace, X-Datamo-Credentials, X-Datamo-Signature, X-Datamo-Timestamp
← Tu app verifica la firma y retorna las métricasEl webhookToken es único por conexión (por cada usuario que conecta tu integración). Debes guardarlo en tu base de datos indexado por workspaceId. Es necesario para:
- Verificar la autenticidad de los pull requests de Datamo (HMAC-SHA256)
- Firmar los eventos push que envíes a Datamo
Configuración en tu servidor Express
import express from 'express'
import { createDatamoRouter } from '@datamo/sdk/express'
const app = express()
// Ruta propia: el apiBaseUrl registrado en Datamo es https://api.tuapp.cl/integrations
app.use('/integrations', createDatamoRouter({
// El clientSecret de tu app (portal de desarrolladores de Datamo): exige la
// firma de Datamo en /datamo/connect.
clientSecret: process.env.DATAMO_CLIENT_SECRET,
// Datamo firma sus requests con HMAC usando el webhookToken de cada workspace.
// Esta función le dice al SDK qué token usar para verificar cada request.
// Retorna null si el workspace no tiene una conexión activa.
getWebhookToken: async (workspaceId: string) => {
const conn = await db.findOne({ workspaceId })
return conn?.webhookToken ?? null
},
// Datamo llama a esta función cada N minutos para obtener métricas actualizadas.
// credentials contiene lo que el usuario ingresó al conectar la integración.
onMetrics: async ({ workspaceId, credentials }) => {
const data = await db.getMetricsForTenant(credentials.tenant_id)
return [
{ key: 'usuarios_activos', label: 'Usuarios activos', value: data.activeUsers },
{ key: 'mrr_usd', label: 'MRR', value: data.mrr, unit: 'USD' },
{ key: 'tasa_retencion', label: 'Tasa de retención', value: data.retention, unit: '%' },
]
},
// Datamo llama a esta función cuando un usuario conecta la integración.
// IMPORTANTE: guarda AMBOS — webhookToken para verificar pulls, webhookUrl para push.
onConnect: async ({ workspaceId, webhookUrl, webhookToken, scopes }) => {
await db.upsert({ workspaceId }, { webhookUrl, webhookToken })
console.log(`Workspace ${workspaceId} conectó Datamo. Scopes: ${scopes.join(', ')}`)
},
}))
app.listen(3000)Esto atiende dos rutas bajo donde lo montes (en el ejemplo, /integrations):
GET /datamo/metrics— endpoint que Datamo sondea para obtener métricasPOST /datamo/connect— endpoint que Datamo llama al conectar un usuario
Desde 0.4.1:
- No necesitas
express.json(): el SDK lee el JSON solo de/datamo/connect(si ya usasexpress.json(), reutiliza ese body). - No toca otras rutas: cualquier otra request pasa de largo sin que se lea su body, así que montarlo en una ruta compartida (
/api) ya no rompe webhooks que necesitan el body crudo. Igual recomendamos una ruta propia. - Express no se carga en tiempo de ejecución:
@datamo/sdk/express(y@datamo/sdk, que lo sigue exportando por compatibilidad) solo usan sus tipos.DatamoPushClienty los helpers funcionan sin express.
Referencia de la API
createDatamoRouter(opts)
Crea un Express Router con los endpoints necesarios para Datamo.
opts.getWebhookToken(workspaceId: string): Promise<string | null> | string | null
Función que retorna el webhookToken almacenado para un workspaceId. El SDK la llama en cada pull request para verificar la firma HMAC. Retorna null si no existe conexión → Datamo recibe 404.
opts.onMetrics(ctx): Promise<DatamoMetric[]> | DatamoMetric[]
Handler invocado cuando Datamo hace pull de métricas. Recibe:
| Campo | Tipo | Descripción |
|---|---|---|
| ctx.workspaceId | string | ID del workspace en Datamo |
| ctx.credentials | Record<string, string> | Credenciales descifradas del usuario |
Debe retornar un array de DatamoMetric:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| key | string | ✅ | Identificador único en snake_case. Ej: "mrr_usd" |
| value | number | ✅ | Valor numérico de la métrica |
| label | string | No | Nombre legible. Si se omite, Datamo usa el key |
| unit | string | No | Unidad. Ej: "USD", "ms", "%" |
| recorded_at | string | No | ISO 8601. Si se omite, Datamo usa la hora de ingesta |
opts.onConnect(payload): Promise<void> | void
Callback opcional invocado cuando un usuario conecta la integración. Recibe DatamoConnectPayload:
| Campo | Tipo | Descripción |
|---|---|---|
| payload.workspaceId | string | ID del workspace en Datamo |
| payload.webhookUrl | string | URL para enviar eventos push. Guardar en DB por workspaceId |
| payload.webhookToken | string | Token secreto para firmar/verificar. Guardar en DB por workspaceId |
| payload.scopes | string[] | Permisos que el usuario aprobó |
opts.clientSecret?: string (desde 0.3.0)
El clientSecret de tu app. Si se configura, POST /datamo/connect exige el header X-Datamo-Connect-Signature (t=<unix>,v1=<hex>): un HMAC-SHA256 con el clientSecret sobre ${t}.${workspaceId}.${webhookToken}.${webhookUrl}.${scopes.join(',')}, con una tolerancia de 5 minutos. Si falta o no es válida, el SDK responde 401 y no llama a onConnect.
Si se omite, el connect se acepta sin firma (comportamiento anterior a 0.3.0) y el SDK muestra una advertencia al arrancar. Recomendado siempre. Las apps registradas antes de este cambio deben rotar su clientSecret una vez para que Datamo empiece a firmar.
opts.onDisconnect(payload): Promise<void> | void (desde 0.4.1)
Datamo llama a POST /datamo/disconnect cuando la empresa desconecta la integración (con reintentos si tu servidor no responde). Recibe { workspaceId }: borra el webhookToken y el webhookUrl guardados para ese workspace. Con clientSecret, el SDK exige X-Datamo-Disconnect-Signature (t=<unix>,v1= HMAC-SHA256 con el clientSecret sobre disconnect.<t>.<workspaceId>); sin él, la firma legacy con el webhookToken.
Firma del pull con el clientSecret (desde 0.4.1)
Además de la firma legacy (con el webhookToken), Datamo firma cada GET /datamo/metrics con X-Datamo-Pull-Signature (t=<unix>,v1= HMAC-SHA256 con el clientSecret sobre pull.<t>.<workspaceId>.<X-Datamo-Credentials>). Con clientSecret configurado, si el header viene, el SDK lo exige válido. Helpers: verifyPullSignature, verifyDisconnectSignature.
DatamoPushClient
Cliente para enviar métricas a Datamo en tiempo real (push model). Usar cuando necesitas notificar a Datamo de un evento inmediatamente, sin esperar el próximo ciclo de pull.
import { DatamoPushClient } from '@datamo/sdk'
// Ejemplo: cuando ocurre una venta en tu app
async function onNewSale(workspaceId: string, amount: number) {
const conn = await db.findOne({ workspaceId })
if (!conn?.webhookUrl) return // este workspace no tiene Datamo conectado
const push = new DatamoPushClient({
webhookUrl: conn.webhookUrl, // guardado en onConnect
webhookToken: conn.webhookToken, // guardado en onConnect
clientSecret: process.env.DATAMO_CLIENT_SECRET, // firma V2 (desde 0.4.0)
})
await push.send([
{ key: 'nueva_venta', value: 1, unit: 'ventas' },
{ key: 'ingresos_hoy', value: amount, unit: 'CLP' },
])
}Opciones
| Opción | Tipo | Descripción |
|---|---|---|
| webhookUrl | string | URL recibida en onConnect |
| webhookToken | string | Token recibido en onConnect |
| clientSecret | string? | El clientSecret de tu app (el mismo de createDatamoRouter). Con él se envía X-Datamo-Signature-V2. Obligatorio en la práctica desde el 2026-11-29 |
Si creas el cliente sin clientSecret, el SDK muestra una advertencia (una vez por proceso) y solo envía la firma legacy.
El clientSecret es uno por app, no por conexión: guárdalo como variable de entorno (DATAMO_CLIENT_SECRET), nunca en el repositorio ni en el frontend.
push.send(metrics: DatamoMetric[]): Promise<DatamoPushResult>
Envía hasta 100 métricas en un solo request. Firma el payload automáticamente con HMAC-SHA256: la firma legacy con el webhookToken y, si configuraste clientSecret, la V2. Lanza un error si Datamo responde con código HTTP de error.
Firma V2 del push (desde 0.4.0)
La firma legacy usa como clave el webhookToken, que también viaja en la URL del webhook (/webhook/<token>/events): quien vea la URL puede firmar. La V2 usa como clave el clientSecret de tu app, que nunca viaja en la URL.
X-Datamo-Signature-V2 = hex( HMAC-SHA256(
key = clientSecret,
msg = "v2." + webhookToken + "." + timestamp + "." + rawBody
) )timestampes el valor exacto del headerX-Datamo-Timestamp(ISO 8601).rawBodyson los bytes exactos del body HTTP enviado.
Transición:
| Periodo | Qué acepta Datamo |
|---|---|
| Hasta el 2026-11-28 | V2 (si viene, se exige y valida) o solo legacy (se acepta con aviso Deprecation) |
| Desde el 2026-11-29 | Solo requests con X-Datamo-Signature-V2 válida |
Durante la transición DatamoPushClient envía ambos headers. Si construyes el request a mano, usa los helpers:
import { signPushV2, pushSigningPayloadV2 } from '@datamo/sdk'
const timestamp = new Date().toISOString()
const body = JSON.stringify({ metrics })
const signatureV2 = signPushV2({ clientSecret, webhookToken, timestamp, rawBody: body })
// headers: X-Datamo-Timestamp: timestamp, X-Datamo-Signature-V2: signatureV2Vector de prueba:
| Entrada | Valor |
|---|---|
| clientSecret | cs_test_datamo_v2 |
| webhookToken | whk_test_token |
| timestamp | 2026-09-30T12:00:00.000Z |
| rawBody | {"metrics":[{"key":"nueva_venta","value":1}]} |
| Firma V2 esperada | e2ad6447ca745c77f9b494fdc0774f54e0bb827a494df4413443b51bb1da205e |
Headers que envía Datamo en los pull requests
Cuando Datamo llama a GET /datamo/metrics, incluye estos headers:
| Header | Descripción |
|---|---|
| X-Datamo-Timestamp | ISO 8601 — momento del request |
| X-Datamo-Workspace | ID del workspace que se está sincronizando |
| X-Datamo-Credentials | JSON base64 con las credenciales descifradas del usuario |
| X-Datamo-Signature | HMAC-SHA256 del request para verificar autenticidad |
El SDK verifica automáticamente la firma y el timestamp: lo acepta dentro de ±5 minutos de tu reloj (desde 0.4.1 la tolerancia es simétrica; antes un reloj de Datamo adelantado en 1 s daba 401).
Algoritmo de firma:
signature = HMAC-SHA256(webhookToken, `${timestamp}.${workspaceId}`)Algoritmo de firma para push (DatamoPushClient → Datamo):
X-Datamo-Signature = HMAC-SHA256(webhookToken, `${timestamp}.${rawBody}`) // legacy, hasta 2026-11-28
X-Datamo-Signature-V2 = HMAC-SHA256(clientSecret, `v2.${webhookToken}.${timestamp}.${rawBody}`) // desde 0.4.0Importante si no usas este SDK para enviar el push (Python, Go, curl, etc.): Datamo verifica la firma sobre los bytes exactos del body HTTP, no sobre una re-serialización de { metrics } en el servidor. Firma exactamente la misma cadena de texto que vas a enviar como body — cualquier diferencia de orden de claves, espaciado, formato de números (1.0 vs 1) o escapado de unicode entre lo que firmas y lo que envías hará que la firma no coincida. DatamoPushClient.send() ya lo hace bien (firma la misma variable body que envía); si construyes el request a mano, asegúrate de firmar el string final, no un objeto que luego serializas de nuevo.
Definición del credentialSchema en el portal de Datamo
Al registrar tu app en el portal de desarrolladores, defines qué campos le pedirle al usuario al conectar la integración. Estos valores llegan descifrados en ctx.credentials en cada call a onMetrics.
Ejemplo para una app con API key por tenant:
authType: api_key
fields:
- key: tenant_id | label: ID de tu cuenta | type: text | required: true
- key: api_key | label: API Key | type: password | required: trueEl usuario verá estos campos en el modal de conexión de Datamo y los valores llegan a tu handler como:
credentials = {
tenant_id: 'acme-corp',
api_key: 'sk-abcdef...'
}Ejemplo completo
import express from 'express'
import { DatamoPushClient } from '@datamo/sdk'
import { createDatamoRouter } from '@datamo/sdk/express'
// Tu modelo de DB (cualquier ORM)
interface DatamoConn { webhookUrl: string; webhookToken: string }
const app = express()
app.use('/integrations', createDatamoRouter({
clientSecret: process.env.DATAMO_CLIENT_SECRET,
getWebhookToken: async (workspaceId) => {
const row = await db.datamo_connections.findOne({ workspaceId })
return row?.webhookToken ?? null
},
onMetrics: async ({ workspaceId, credentials }) => {
const stats = await myService.getStats(credentials.tenant_id, credentials.api_key)
return [
{ key: 'usuarios_activos', value: stats.activeUsers },
{ key: 'sesiones_hoy', value: stats.sessionsToday },
{ key: 'mrr_usd', value: stats.mrr, unit: 'USD' },
{ key: 'tasa_conversion', value: stats.conversionRate, unit: '%' },
]
},
onConnect: async ({ workspaceId, webhookUrl, webhookToken }) => {
await db.datamo_connections.upsert(
{ workspaceId },
{ webhookUrl, webhookToken },
)
},
}))
// Push en tiempo real: llamar cuando ocurra algo relevante
export async function pushMetricsToDatamo(workspaceId: string, metrics: { key: string; value: number }[]) {
const conn = await db.datamo_connections.findOne({ workspaceId })
if (!conn) return
const client = new DatamoPushClient({ ...conn, clientSecret: process.env.DATAMO_CLIENT_SECRET })
await client.send(metrics)
}
app.listen(process.env.PORT || 3000)Seguridad
- Las credenciales del usuario viajan cifradas con AES-256-GCM dentro de Datamo y solo se descifran en el momento del pull request, transmitiéndose en el header
X-Datamo-Credentials(base64 JSON) exclusivamente a tuapiBaseUrlpor HTTPS. - El
webhookTokenes un secreto de 64 caracteres hex (32 bytes) único por conexión. Trátalo como una contraseña. - Todos los requests de Datamo incluyen firma HMAC-SHA256 con timestamp para proteger contra replay attacks (ventana de 5 minutos).
POST /datamo/connectse verifica con tuclientSecret(opts.clientSecret, desde 0.3.0). Sin él, cualquiera que conozca unworkspaceIdpodría reemplazar elwebhookTokenguardado y cortar la sincronización de ese cliente.- El push se firma con tu
clientSecret(X-Datamo-Signature-V2, desde 0.4.0). La firma legacy con elwebhookTokendeja de aceptarse el 2026-11-29.
Tipos TypeScript exportados
import type {
DatamoMetric,
DatamoCredentials,
DatamoMetricsContext,
DatamoConnectPayload,
DatamoRouterOptions,
DatamoPushOptions,
DatamoPushResult,
} from '@datamo/sdk'Helpers de firma exportados: verifyDatamoSignature, isTimestampFresh, verifyConnectSignature, connectSigningPayload, signPushV2, pushSigningPayloadV2 y la constante LEGACY_PUSH_SIGNATURE_SUNSET ('2026-11-29').
