Para desarrolladores · Plan Pro

Conecta tu sistema con SocIA

Lee y registra clientes, productos, ventas, gastos y membresías desde tu CRM, tu ERP o tu tienda en línea. Y recibe un aviso al instante cada vez que pasa algo en tu negocio, venga de la app, de WhatsApp o de la IA.

Claves con permiso de lectura o escritura Webhooks firmados Todo pasa por las mismas reglas de la app

Empezar en 3 pasos

  1. 1

    Ten el plan Pro. Lo cambias en Ajustes → Suscripción.

  2. 2

    Entra a Ajustes → Desarrolladores y toca Nueva clave. Ponle el nombre de tu sistema y elige si solo lee o también escribe. Copia la clave: se ve completa una sola vez.

  3. 3

    Guárdala en una variable de entorno de tu servidor y haz tu primera llamada:

export SOCIA_KEY="socia_tu_clave"

curl "https://tusociapp.com/api/v1/customers?limit=5" \
  -H "Authorization: Bearer $SOCIA_KEY"

La dirección base es https://tusociapp.com/api/v1. Todo va y viene en JSON. Si prefieres una herramienta, la especificación OpenAPI 3.1 sirve para Postman, Insomnia o para generar un cliente.

Autenticación

Manda tu clave en cada llamada, en el encabezado Authorization: Bearer socia_…. Hay dos tipos:

  • Solo lectura: consulta todo. No gasta créditos.
  • Lectura y escritura: además crea y cambia clientes, productos, ventas y gastos.

Puedes tener hasta 5 claves activas. Úsalas solo desde tu servidor, nunca en una página web o una app que otros puedan abrir. Si una se filtra, revócala en Ajustes → Desarrolladores: deja de funcionar enseguida. Solo el dueño o un administrador puede crearlas, y la clave trabaja con los permisos de quien la creó.

Formato

  • El dinero va en texto con dos decimales y con su moneda: "price": "1.50", "currency": "USD". Al mandar, puedes usar texto o número. Los precios y totales van en dólares; los bolívares se calculan con la tasa BCV del día.
  • Las fechas van en ISO 8601, en UTC: 2026-10-13T14:00:00.000Z. Los días (como date o due_date) van como 2026-10-13.
  • Cada cosa tiene un id y un object que dice qué es. En los ejemplos, cambia lo que está en MAYÚSCULAS (como ID_DEL_CLIENTE) por el id real.
  • Idempotency-Key: al crear, manda un encabezado Idempotency-Key con un valor único (por ejemplo, el id en tu sistema). Si reintentas con el mismo valor, no se crea dos veces ni se cobra dos veces: te devolvemos lo mismo, con Idempotent-Replayed: true. Vale 24 horas.
  • Todo lo que crees o cambies queda en Actividad, con el origen «API», y se puede deshacer desde la app como cualquier otra cosa.

Clientes

nametexto
Nombre. Obligatorio al crear; no se puede repetir.
doc_idtexto
Cédula o RIF.
phone, email, address, notestexto
Contacto y notas.
credit_limitdinero
Hasta cuánto le fías (USD).
balancedinero
Lo que debe hoy. Solo lectura.
GET/customers· Listar
curl "https://tusociapp.com/api/v1/customers?limit=50" \
  -H "Authorization: Bearer $SOCIA_KEY"
GET/customers/{id}· Ver uno
curl "https://tusociapp.com/api/v1/customers/ID_DEL_CLIENTE" \
  -H "Authorization: Bearer $SOCIA_KEY"
POST/customers· Crear
curl "https://tusociapp.com/api/v1/customers" \
  -H "Authorization: Bearer $SOCIA_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cliente-0001" \
  -d '{
    "name": "Ana Ejemplo",
    "doc_id": "V12345678",
    "phone": "04140000000",
    "credit_limit": "50.00"
  }'
PATCH/customers/{id}· Cambiar

Manda solo lo que cambia.

curl -X PATCH "https://tusociapp.com/api/v1/customers/ID_DEL_CLIENTE" \
  -H "Authorization: Bearer $SOCIA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "04240000000",
    "notes": "Prefiere pago móvil"
  }'
Respuesta: un cliente
{
  "object": "customer",
  "id": "0b9f2c4e-1a2b-4c3d-8e9f-001122334455",
  "name": "Ana Ejemplo",
  "doc_id": "V12345678",
  "phone": "04140000000",
  "email": null,
  "address": null,
  "notes": null,
  "currency": "USD",
  "credit_limit": "50.00",
  "balance": "0.00",
  "created_at": "2026-10-13T14:00:00.000Z",
  "updated_at": "2026-10-13T14:00:00.000Z"
}

Productos

nametexto
Nombre. Obligatorio al crear.
skutexto
Código o código de barras.
category, unittexto
Categoría y unidad (unidad, kg, litro…).
price, costdinero
Precio de venta y costo, en USD.
stocknúmero
Existencia. Al cambiarla, queda anotada como ajuste de inventario.
min_stocknúmero
Para el aviso de inventario bajo.
track_stock, activesí/no
Si lleva inventario y si está a la venta.
GET/products· Listar lo que cambió
curl "https://tusociapp.com/api/v1/products?updated_since=2026-10-01T00%3A00%3A00Z" \
  -H "Authorization: Bearer $SOCIA_KEY"
GET/products/{id}· Ver uno
curl "https://tusociapp.com/api/v1/products/ID_DEL_PRODUCTO" \
  -H "Authorization: Bearer $SOCIA_KEY"
POST/products· Crear
curl "https://tusociapp.com/api/v1/products" \
  -H "Authorization: Bearer $SOCIA_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: producto-0001" \
  -d '{
    "name": "Harina de maíz 1 kg",
    "sku": "7590000000011",
    "price": "1.50",
    "cost": "1.10",
    "stock": 24,
    "min_stock": 6,
    "unit": "unidad"
  }'
PATCH/products/{id}· Cambiar precio y existencia
curl -X PATCH "https://tusociapp.com/api/v1/products/ID_DEL_PRODUCTO" \
  -H "Authorization: Bearer $SOCIA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "price": "1.60",
    "stock": 30
  }'
Respuesta: un producto
{
  "object": "product",
  "id": "5c7d1e2f-3a4b-4c5d-9e8f-665544332211",
  "name": "Harina de maíz 1 kg",
  "sku": "7590000000011",
  "category": null,
  "unit": "unidad",
  "currency": "USD",
  "price": "1.50",
  "cost": "1.10",
  "stock": 24,
  "min_stock": 6,
  "track_stock": true,
  "active": true,
  "parent_id": null,
  "attributes": null,
  "created_at": "2026-10-13T14:00:00.000Z",
  "updated_at": "2026-10-13T14:05:00.000Z"
}

Ventas

Una venta descuenta inventario, entra a la caja y, si es fiada, queda en lo que te deben: igual que en la app. Si no mandas pagos ni on_credit, queda pagada en efectivo en dólares.

itemslista
Los productos: product_id (o description), quantity y, si quieres, unit_price.
totaldinero
Si no mandas items, el total en USD.
customer_idtexto
El cliente (obligatorio si es fiada).
paymentslista
method, amount, currency y reference. Sin amount, cubre lo que falte.
on_credit, due_datesí/no, día
Fiada: lo que no se pagó queda como deuda del cliente.
date, notesdía, texto
Fecha de la venta y nota.
sede_idtexto
En qué sede se vendió. Obligatorio si el negocio tiene varias sedes.

Métodos de pago: efectivo_usd, efectivo_bs, pago_movil, transferencia, punto_venta, biopago, zelle y usdt. Si mandas dólares en un método en bolívares (como pago móvil), se convierte con la tasa BCV.

GET/sales· Listar
curl "https://tusociapp.com/api/v1/sales?limit=20" \
  -H "Authorization: Bearer $SOCIA_KEY"
GET/sales/{id}· Ver una
curl "https://tusociapp.com/api/v1/sales/ID_DE_LA_VENTA" \
  -H "Authorization: Bearer $SOCIA_KEY"
POST/sales· Crear
curl "https://tusociapp.com/api/v1/sales" \
  -H "Authorization: Bearer $SOCIA_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: venta-0001" \
  -d '{
    "items": [
      {
        "product_id": "ID_DEL_PRODUCTO",
        "quantity": 2
      }
    ],
    "payments": [
      {
        "method": "pago_movil",
        "reference": "001234"
      }
    ]
  }'
Respuesta: una venta
{
  "object": "sale",
  "id": "9a8b7c6d-5e4f-4a3b-8c2d-112233445566",
  "number": 128,
  "status": "paid",
  "customer_id": null,
  "sold_at": "2026-10-13T14:10:00.000Z",
  "currency": "USD",
  "total": "3.00",
  "due": "0.00",
  "rate": "200.00",
  "total_ves": "600.00",
  "notes": null,
  "items": [
    { "product_id": "5c7d1e2f-…", "description": "Harina de maíz 1 kg", "quantity": 2, "unit_price": "1.50", "total": "3.00" }
  ],
  "payments": [
    { "method": "pago_movil", "currency": "VES", "amount": "600.00", "amount_usd": "3.00", "rate": "200.00", "reference": "001234", "paid_at": "…" }
  ],
  "created_at": "2026-10-13T14:10:00.000Z",
  "updated_at": "2026-10-13T14:10:00.000Z"
}

Gastos

descriptiontexto
Qué fue. Obligatorio.
amount, currencydinero, texto
El monto en USD o VES. Obligatorio.
categorytexto
alquiler, servicios, transporte… Por defecto «otros».
method, referencetexto
Cómo se pagó y la referencia.
datedía
Cuándo fue. Por defecto, hoy.
sede_idtexto
En qué sede fue. Obligatorio si el negocio tiene varias sedes.
GET/expenses· Listar
curl "https://tusociapp.com/api/v1/expenses?updated_since=2026-10-01T00%3A00%3A00Z" \
  -H "Authorization: Bearer $SOCIA_KEY"
GET/expenses/{id}· Ver uno
curl "https://tusociapp.com/api/v1/expenses/ID_DEL_GASTO" \
  -H "Authorization: Bearer $SOCIA_KEY"
POST/expenses· Crear
curl "https://tusociapp.com/api/v1/expenses" \
  -H "Authorization: Bearer $SOCIA_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: gasto-0001" \
  -d '{
    "description": "Pago de luz",
    "category": "servicios",
    "amount": "1500.00",
    "currency": "VES",
    "method": "pago_movil",
    "reference": "004321"
  }'
Respuesta: un gasto
{
  "object": "expense",
  "id": "4d3c2b1a-0f9e-4d8c-a7b6-998877665544",
  "description": "Pago de luz",
  "category": "servicios",
  "currency": "VES",
  "amount": "1500.00",
  "amount_usd": "7.50",
  "rate": "200.00",
  "method": "pago_movil",
  "reference": "004321",
  "recurring": null,
  "spent_at": "2026-10-13T12:00:00.000Z",
  "created_at": "2026-10-13T14:20:00.000Z",
  "updated_at": "2026-10-13T14:20:00.000Z"
}

Sedes

Si el negocio tiene varias tiendas o sucursales, cada venta, gasto y pago trae sede_id. Con una sola sede no hace falta mandarlo. Los traslados mueven mercancía entre sedes: van sent (en camino) y quedan received cuando la sede que recibe lo confirma.

GET/sedes· Listar las sedes
curl "https://tusociapp.com/api/v1/sedes" \
  -H "Authorization: Bearer $SOCIA_KEY"
GET/products/{id}/stock· Existencias de un producto en cada sede

Cada sede trae quantity, min_stock, su price, si se vende ahí (sold_here) y lo que viene en camino (incoming).

curl "https://tusociapp.com/api/v1/products/ID_DEL_PRODUCTO/stock" \
  -H "Authorization: Bearer $SOCIA_KEY"
GET/transfers· Listar traslados
curl "https://tusociapp.com/api/v1/transfers?limit=20" \
  -H "Authorization: Bearer $SOCIA_KEY"
GET/transfers/{id}· Ver un traslado
curl "https://tusociapp.com/api/v1/transfers/ID_DEL_TRASLADO" \
  -H "Authorization: Bearer $SOCIA_KEY"

Membresías

Con el módulo Membresías encendido. Cada cobro queda como una venta al cliente, con la tasa BCV del día del pago. status es active, expiring, expired, paused o cancelled; ends_on es el día en que vence.

customer_id o customer_nametexto
Quién se inscribe. Con un nombre nuevo, se crea el cliente.
plan_idtexto
El plan. Obligatorio.
periodsnúmero
Cuántos períodos del plan paga de una vez. Por defecto 1.
phone, email, reminderstexto, sí/no
Su contacto y si aceptó recordatorios por WhatsApp.
method, reference, datetexto, día
Cómo y cuándo pagó. Con on_credit queda debiendo; con charge en false no se cobra.
GET/memberships· Listar

Filtros: status, customer_id y expiring_within_days. Para buscar por teléfono o correo usa POST /memberships/search con los mismos filtros en el cuerpo, así no quedan en los registros.

curl "https://tusociapp.com/api/v1/memberships?status=expiring&expiring_within_days=7" \
  -H "Authorization: Bearer $SOCIA_KEY"
POST/memberships/search· Buscar por teléfono o correo
curl "https://tusociapp.com/api/v1/memberships/search" \
  -H "Authorization: Bearer $SOCIA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+584140000000"
  }'
POST/memberships/check· ¿Está al día?

Responde { active, status, plan, until, daysLeft }. Sirve para tu torniquete o tu página de socios. En pausa, active es false. Con phone o email en la dirección (GET) sigue funcionando, pero está obsoleto.

curl "https://tusociapp.com/api/v1/memberships/check" \
  -H "Authorization: Bearer $SOCIA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+584140000000"
  }'
GET/memberships/{id}· Ver una
curl "https://tusociapp.com/api/v1/memberships/ID_DE_LA_MEMBRESIA" \
  -H "Authorization: Bearer $SOCIA_KEY"
POST/memberships· Inscribir
curl "https://tusociapp.com/api/v1/memberships" \
  -H "Authorization: Bearer $SOCIA_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: inscripcion-0001" \
  -d '{
    "customer_name": "Ana Ejemplo",
    "plan_id": "ID_DEL_PLAN",
    "periods": 1,
    "method": "pago_movil"
  }'
POST/memberships/{id}/renew· Renovar

Suma desde el día en que vence (o desde el pago, si ya venció), sin perder días.

curl "https://tusociapp.com/api/v1/memberships/ID_DE_LA_MEMBRESIA/renew" \
  -H "Authorization: Bearer $SOCIA_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: renovacion-0001" \
  -d '{
    "periods": 3,
    "method": "zelle"
  }'
POST/memberships/{id}/pause· Pausar

Los días de la pausa se suman al vencimiento.

curl "https://tusociapp.com/api/v1/memberships/ID_DE_LA_MEMBRESIA/pause" \
  -H "Authorization: Bearer $SOCIA_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pausa-0001" \
  -d '{
    "days": 14
  }'
POST/memberships/{id}/cancel· Cancelar
curl "https://tusociapp.com/api/v1/memberships/ID_DE_LA_MEMBRESIA/cancel" \
  -H "Authorization: Bearer $SOCIA_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cancelar-0001" \
  -d '{}'

Enlaces de pago

Un enlace para que el cliente pague su membresía: ve el monto en $ y en Bs a la tasa BCV de hoy, cómo pagarle al negocio, y avisa cuando pagó. El negocio lo aprueba y ahí se renueva la membresía y queda la venta. status es open, reported (avisó que pagó), paid, expired o cancelled.

POST/memberships/{id}/payment-link· Crear un enlace

Responde { id, url, token, amountUsd, expiresAt }. El monto es el precio del plan por los períodos. Si ya tiene uno abierto por lo mismo, devuelve ese. Con expires_in_days (1 a 60) cambias cuánto dura.

curl "https://tusociapp.com/api/v1/memberships/ID_DE_LA_MEMBRESIA/payment-link" \
  -H "Authorization: Bearer $SOCIA_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: enlace-0001" \
  -d '{
    "periods": 1
  }'
GET/payment-requests· Listar cobros

Filtros: status, customer_id y membership_id.

curl "https://tusociapp.com/api/v1/payment-requests?status=reported" \
  -H "Authorization: Bearer $SOCIA_KEY"
GET/payment-requests/{id}· Ver un cobro
curl "https://tusociapp.com/api/v1/payment-requests/ID_DEL_COBRO" \
  -H "Authorization: Bearer $SOCIA_KEY"

Pedidos de la tienda

Los pedidos que llegan por la tienda en línea, con el módulo Tienda en línea. status es new, accepted, paid, ready (listo para retirar), on_the_way, delivered, rejected o cancelled. Cuando el negocio aprueba el pago, sale_id trae la venta.

GET/orders· Listar pedidos

Paginados como las ventas. Filtro: status.

curl "https://tusociapp.com/api/v1/orders?status=new" \
  -H "Authorization: Bearer $SOCIA_KEY"
GET/orders/{id}· Ver un pedido
curl "https://tusociapp.com/api/v1/orders/ID_DEL_PEDIDO" \
  -H "Authorization: Bearer $SOCIA_KEY"

Paginación

Las listas traen hasta limit cosas (por defecto 50, máximo 100), de la más antigua a la más nueva según su último cambio. Si has_more es true, pide la siguiente con cursor = el next_cursor que te llegó.

Para sincronizar, guarda la hora de tu última sincronización y pide solo lo que cambió desde entonces con updated_since.

Respuesta: una lista
{
  "object": "list",
  "data": [ { "object": "customer", "id": "…", "name": "Ana Ejemplo", … } ],
  "has_more": true,
  "next_cursor": "WyIyMDI2LTEwLTEzIDE0OjAwOjAwLjEyMzQ1NiswMCIsIi4uLiJd"
}
curl "https://tusociapp.com/api/v1/customers?limit=2" \
  -H "Authorization: Bearer $SOCIA_KEY"

# Para la siguiente página, manda el next_cursor que te llegó:
curl "https://tusociapp.com/api/v1/customers?limit=2&cursor=EL_NEXT_CURSOR" \
  -H "Authorization: Bearer $SOCIA_KEY"

Errores

Si algo sale mal, la respuesta trae un código para tu programa y un mensaje en español para mostrar. Lo que no se pudo hacer no queda a medias.

{
  "error": {
    "code": "invalid_request",
    "message": "credit_limit: escribe un número decimal, por ejemplo \"12.50\"",
    "field": "credit_limit"
  }
}
400invalid_request, invalid_json, invalid_cursor
Falta un dato o está mal escrito. `field` dice cuál.
401unauthenticated, invalid_key
No mandaste la clave, o no existe o fue revocada.
402no_credits
Se acabaron los créditos: las escrituras se detienen; las lecturas siguen.
403insufficient_scope, plan_required, module_disabled, forbidden
La clave es de solo lectura, el plan no es Pro, el módulo está apagado o quien creó la clave ya no administra el negocio.
404not_found
No existe en este negocio (o la ruta no existe).
409idempotency_in_progress
La misma Idempotency-Key se está procesando todavía.
422idempotency_mismatch
Usaste una Idempotency-Key que ya usaste con otros datos.
429rate_limited, daily_limit
Demasiadas llamadas. Espera los segundos de Retry-After.
500internal
Algo falló de nuestro lado y no se guardó nada. Ya nos llegó el aviso; intenta de nuevo.

Límites

Cada clave puede hacer 60 llamadas por minuto y 10.000 al día (el día cambia a medianoche, hora de Venezuela). Cada respuesta te dice cuánto te queda:

RateLimit-Limit: 60
RateLimit-Remaining: 59
RateLimit-Reset: 42
X-RateLimit-Limit-Day: 10000
X-RateLimit-Remaining-Day: 9958

Si te pasas, recibes 429 con Retry-After: los segundos que debes esperar. Para cargar muchas cosas, ve una por una y respeta esa espera.

Créditos

  • Leer es gratis.
  • Cada cosa que se crea o se cambia bien gasta 1 crédito de tu plan. Si falla, no se cobra; si repites con la misma Idempotency-Key, tampoco.
  • Si te quedas sin créditos, las escrituras responden 402 con no_credits y las lecturas siguen funcionando. Recarga en Ajustes → Suscripción.
  • Los webhooks no gastan créditos.

Webhooks

En Ajustes → Desarrolladores, toca Nuevo webhook, escribe la dirección de tu sistema (con https://) y elige qué quieres saber. Puedes tener hasta 3. Te mostramos un secreto una sola vez: con él revisas que el aviso viene de nosotros. Usa Probar para mandar un aviso de prueba (ping).

sale.created
Se registró una venta. Trae la venta completa, con sus productos y pagos.
customer.created
Hay un cliente nuevo (también cuando se crea al vender).
customer.updated
Cambiaron los datos de un cliente.
product.updated
Cambió un producto: precio, nombre o existencia (por ejemplo, después de una venta).
expense.created
Se registró un gasto.
stock.low
Un producto llegó a su mínimo. Llega una vez cada vez que baja del mínimo.
membership.created
Alguien se inscribió en un plan (desde la app, WhatsApp, la API o al aprobar una solicitud).
membership.renewed
Se renovó una membresía. Trae el nuevo vencimiento.
membership.expiring
A una membresía le quedan pocos días (los que el negocio configuró). Llega una vez por vencimiento.
membership.expired
Una membresía venció y pasaron los días de gracia sin renovar. Llega una vez.
membership.request.created
Alguien llenó el formulario de inscripción y espera que el negocio lo apruebe.
payment_request.reported
Un cliente avisó desde su enlace de pago que ya pagó. Trae la forma de pago, la referencia y el monto.
payment_request.paid
El negocio aprobó un pago por enlace: la membresía ya está renovada.
payment_request.rejected
El negocio rechazó un pago por enlace. Trae el motivo; el enlace vuelve a quedar abierto.
transfer.sent
Una sede envió mercancía a otra. Mientras viene en camino no se vende en ninguna de las dos.
transfer.received
La sede que recibe confirmó el traslado. Trae lo que llegó de cada producto, por si faltó algo.
order.created
Llegó un pedido de la tienda en línea. Trae lo que pidió, la entrega y el total.
order.paid
El negocio aprobó el pago de un pedido: ya hay venta (sale_id) y se descontó el inventario.
order.status_changed
Un pedido cambió de estado: aceptado, pagado, listo, en camino, entregado, rechazado o cancelado. Trae el motivo si lo hay.

Cada aviso es un POST con JSON. En data va lo mismo que te da la API; source dice de dónde vino: app, whatsapp, ai o api.

Lo que te llega
POST /tu-ruta HTTP/1.1
Content-Type: application/json
User-Agent: SocIA-Webhooks/1.0
X-SocIA-Event: sale.created
X-SocIA-Delivery: 7e6d5c4b-…
X-SocIA-Signature: t=1792000000,v1=5f0c3a…

{
  "id": "evt_2b1f0e9d8c7b6a5f4e3d2c1b0a998877",
  "type": "sale.created",
  "created_at": "2026-10-13T14:10:00.000Z",
  "source": "whatsapp",
  "business_id": "…",
  "data": { "object": "sale", "id": "…", "total": "3.00", … }
}

Responde rápido, y no proceses dos veces

  • Responde con un código 2xx en menos de 5 segundos. Si tienes que hacer algo largo, hazlo después de responder.
  • Si no responde bien, reintentamos a los 1 min, 10 min, 1 h, 4 h: 5 intentos en unas horas. El id del aviso es el mismo en cada reintento: úsalo para no procesarlo dos veces.
  • Si 5 avisos seguidos no llegan, apagamos el webhook y le avisamos al dueño. Lo vuelves a activar en Ajustes cuando tu sistema esté bien.
  • En Ajustes ves los últimos 50 envíos: si llegaron, qué respondió tu sistema o por qué falló.
  • Solo mandamos a direcciones públicas con https://. No seguimos redirecciones.

Revisar la firma

Cada aviso trae X-SocIA-Signature: t=…,v1=…. t es la hora (en segundos) y v1 es el HMAC-SHA256, en hexadecimal, de t + "." + el cuerpo tal cual llegó, con tu secreto. Calcula lo mismo y compáralo. Rechaza los avisos de hace más de 5 minutos para que nadie pueda repetir uno viejo.

import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";

const SECRET = process.env.SOCIA_WEBHOOK_SECRET;

function isFromSocia(header, body) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = createHmac("sha256", SECRET).update(`${parts.t}.${body}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && parts.v1?.length === expected.length && timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}

createServer((req, res) => {
  let body = "";
  req.on("data", (chunk) => (body += chunk));
  req.on("end", () => {
    if (!isFromSocia(req.headers["x-socia-signature"] ?? "", body)) return res.writeHead(400).end();
    const event = JSON.parse(body);
    console.log(event.type, event.data.id);
    res.writeHead(200).end();
  });
}).listen(3000);

Usa el cuerpo exacto que llegó, antes de convertirlo a JSON: si lo vuelves a armar, la firma no coincide.

¿Listo para conectar tu sistema?

La API y los webhooks vienen con el plan Pro. Crea tu clave en Ajustes → Desarrolladores.