Empezar en 3 pasos
- 1
Ten el plan Pro. Lo cambias en Ajustes → Suscripción.
- 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
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"// SOCIA_KEY=socia_tu_clave node primera.mjs
const res = await fetch("https://tusociapp.com/api/v1/customers?limit=5", {
headers: { Authorization: `Bearer ${process.env.SOCIA_KEY}` },
});
const data = await res.json();
console.log(res.status, data);# SOCIA_KEY=socia_tu_clave python primera.py
import os
import requests
res = requests.get(
"https://tusociapp.com/api/v1/customers",
params={
"limit": 5
},
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
)
print(res.status_code, res.json())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 (comodateodue_date) van como2026-10-13. - Cada cosa tiene un
idy unobjectque dice qué es. En los ejemplos, cambia lo que está enMAYÚSCULAS(comoID_DEL_CLIENTE) por el id real. - Idempotency-Key: al crear, manda un encabezado
Idempotency-Keycon 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, conIdempotent-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
| Campo | Tipo | Qué es |
|---|---|---|
| name | texto | Nombre. Obligatorio al crear; no se puede repetir. |
| doc_id | texto | Cédula o RIF. |
| phone, email, address, notes | texto | Contacto y notas. |
| credit_limit | dinero | Hasta cuánto le fías (USD). |
| balance | dinero | Lo que debe hoy. Solo lectura. |
- 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.
/customers· Listarcurl "https://tusociapp.com/api/v1/customers?limit=50" \
-H "Authorization: Bearer $SOCIA_KEY"const res = await fetch("https://tusociapp.com/api/v1/customers?limit=50", {
headers: { Authorization: `Bearer ${process.env.SOCIA_KEY}` },
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.get(
"https://tusociapp.com/api/v1/customers",
params={
"limit": 50
},
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
)
print(res.status_code, res.json())/customers/{id}· Ver unocurl "https://tusociapp.com/api/v1/customers/ID_DEL_CLIENTE" \
-H "Authorization: Bearer $SOCIA_KEY"const res = await fetch("https://tusociapp.com/api/v1/customers/ID_DEL_CLIENTE", {
headers: { Authorization: `Bearer ${process.env.SOCIA_KEY}` },
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.get(
"https://tusociapp.com/api/v1/customers/ID_DEL_CLIENTE",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
)
print(res.status_code, res.json())/customers· Crearcurl "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"
}'const res = await fetch("https://tusociapp.com/api/v1/customers", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SOCIA_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "cliente-0001",
},
body: JSON.stringify({
"name": "Ana Ejemplo",
"doc_id": "V12345678",
"phone": "04140000000",
"credit_limit": "50.00"
}),
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.post(
"https://tusociapp.com/api/v1/customers",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}", "Idempotency-Key": "cliente-0001"},
json={
"name": "Ana Ejemplo",
"doc_id": "V12345678",
"phone": "04140000000",
"credit_limit": "50.00"
},
)
print(res.status_code, res.json())/customers/{id}· CambiarManda 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"
}'const res = await fetch("https://tusociapp.com/api/v1/customers/ID_DEL_CLIENTE", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.SOCIA_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"phone": "04240000000",
"notes": "Prefiere pago móvil"
}),
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.patch(
"https://tusociapp.com/api/v1/customers/ID_DEL_CLIENTE",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
json={
"phone": "04240000000",
"notes": "Prefiere pago móvil"
},
)
print(res.status_code, res.json()){
"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
| Campo | Tipo | Qué es |
|---|---|---|
| name | texto | Nombre. Obligatorio al crear. |
| sku | texto | Código o código de barras. |
| category, unit | texto | Categoría y unidad (unidad, kg, litro…). |
| price, cost | dinero | Precio de venta y costo, en USD. |
| stock | número | Existencia. Al cambiarla, queda anotada como ajuste de inventario. |
| min_stock | número | Para el aviso de inventario bajo. |
| track_stock, active | sí/no | Si lleva inventario y si está a la venta. |
- 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.
/products· Listar lo que cambiócurl "https://tusociapp.com/api/v1/products?updated_since=2026-10-01T00%3A00%3A00Z" \
-H "Authorization: Bearer $SOCIA_KEY"const res = await fetch("https://tusociapp.com/api/v1/products?updated_since=2026-10-01T00%3A00%3A00Z", {
headers: { Authorization: `Bearer ${process.env.SOCIA_KEY}` },
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.get(
"https://tusociapp.com/api/v1/products",
params={
"updated_since": "2026-10-01T00:00:00Z"
},
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
)
print(res.status_code, res.json())/products/{id}· Ver unocurl "https://tusociapp.com/api/v1/products/ID_DEL_PRODUCTO" \
-H "Authorization: Bearer $SOCIA_KEY"const res = await fetch("https://tusociapp.com/api/v1/products/ID_DEL_PRODUCTO", {
headers: { Authorization: `Bearer ${process.env.SOCIA_KEY}` },
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.get(
"https://tusociapp.com/api/v1/products/ID_DEL_PRODUCTO",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
)
print(res.status_code, res.json())/products· Crearcurl "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"
}'const res = await fetch("https://tusociapp.com/api/v1/products", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SOCIA_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "producto-0001",
},
body: JSON.stringify({
"name": "Harina de maíz 1 kg",
"sku": "7590000000011",
"price": "1.50",
"cost": "1.10",
"stock": 24,
"min_stock": 6,
"unit": "unidad"
}),
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.post(
"https://tusociapp.com/api/v1/products",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}", "Idempotency-Key": "producto-0001"},
json={
"name": "Harina de maíz 1 kg",
"sku": "7590000000011",
"price": "1.50",
"cost": "1.10",
"stock": 24,
"min_stock": 6,
"unit": "unidad"
},
)
print(res.status_code, res.json())/products/{id}· Cambiar precio y existenciacurl -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
}'const res = await fetch("https://tusociapp.com/api/v1/products/ID_DEL_PRODUCTO", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.SOCIA_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"price": "1.60",
"stock": 30
}),
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.patch(
"https://tusociapp.com/api/v1/products/ID_DEL_PRODUCTO",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
json={
"price": "1.60",
"stock": 30
},
)
print(res.status_code, res.json()){
"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.
| Campo | Tipo | Qué es |
|---|---|---|
| items | lista | Los productos: product_id (o description), quantity y, si quieres, unit_price. |
| total | dinero | Si no mandas items, el total en USD. |
| customer_id | texto | El cliente (obligatorio si es fiada). |
| payments | lista | method, amount, currency y reference. Sin amount, cubre lo que falte. |
| on_credit, due_date | sí/no, día | Fiada: lo que no se pagó queda como deuda del cliente. |
| date, notes | día, texto | Fecha de la venta y nota. |
| sede_id | texto | En qué sede se vendió. Obligatorio si el negocio tiene varias sedes. |
- 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.
/sales· Listarcurl "https://tusociapp.com/api/v1/sales?limit=20" \
-H "Authorization: Bearer $SOCIA_KEY"const res = await fetch("https://tusociapp.com/api/v1/sales?limit=20", {
headers: { Authorization: `Bearer ${process.env.SOCIA_KEY}` },
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.get(
"https://tusociapp.com/api/v1/sales",
params={
"limit": 20
},
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
)
print(res.status_code, res.json())/sales/{id}· Ver unacurl "https://tusociapp.com/api/v1/sales/ID_DE_LA_VENTA" \
-H "Authorization: Bearer $SOCIA_KEY"const res = await fetch("https://tusociapp.com/api/v1/sales/ID_DE_LA_VENTA", {
headers: { Authorization: `Bearer ${process.env.SOCIA_KEY}` },
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.get(
"https://tusociapp.com/api/v1/sales/ID_DE_LA_VENTA",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
)
print(res.status_code, res.json())/sales· Crearcurl "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"
}
]
}'const res = await fetch("https://tusociapp.com/api/v1/sales", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SOCIA_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "venta-0001",
},
body: JSON.stringify({
"items": [
{
"product_id": "ID_DEL_PRODUCTO",
"quantity": 2
}
],
"payments": [
{
"method": "pago_movil",
"reference": "001234"
}
]
}),
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.post(
"https://tusociapp.com/api/v1/sales",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}", "Idempotency-Key": "venta-0001"},
json={
"items": [
{
"product_id": "ID_DEL_PRODUCTO",
"quantity": 2
}
],
"payments": [
{
"method": "pago_movil",
"reference": "001234"
}
]
},
)
print(res.status_code, res.json()){
"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
| Campo | Tipo | Qué es |
|---|---|---|
| description | texto | Qué fue. Obligatorio. |
| amount, currency | dinero, texto | El monto en USD o VES. Obligatorio. |
| category | texto | alquiler, servicios, transporte… Por defecto «otros». |
| method, reference | texto | Cómo se pagó y la referencia. |
| date | día | Cuándo fue. Por defecto, hoy. |
| sede_id | texto | En qué sede fue. Obligatorio si el negocio tiene varias sedes. |
- 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.
/expenses· Listarcurl "https://tusociapp.com/api/v1/expenses?updated_since=2026-10-01T00%3A00%3A00Z" \
-H "Authorization: Bearer $SOCIA_KEY"const res = await fetch("https://tusociapp.com/api/v1/expenses?updated_since=2026-10-01T00%3A00%3A00Z", {
headers: { Authorization: `Bearer ${process.env.SOCIA_KEY}` },
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.get(
"https://tusociapp.com/api/v1/expenses",
params={
"updated_since": "2026-10-01T00:00:00Z"
},
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
)
print(res.status_code, res.json())/expenses/{id}· Ver unocurl "https://tusociapp.com/api/v1/expenses/ID_DEL_GASTO" \
-H "Authorization: Bearer $SOCIA_KEY"const res = await fetch("https://tusociapp.com/api/v1/expenses/ID_DEL_GASTO", {
headers: { Authorization: `Bearer ${process.env.SOCIA_KEY}` },
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.get(
"https://tusociapp.com/api/v1/expenses/ID_DEL_GASTO",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
)
print(res.status_code, res.json())/expenses· Crearcurl "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"
}'const res = await fetch("https://tusociapp.com/api/v1/expenses", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SOCIA_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "gasto-0001",
},
body: JSON.stringify({
"description": "Pago de luz",
"category": "servicios",
"amount": "1500.00",
"currency": "VES",
"method": "pago_movil",
"reference": "004321"
}),
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.post(
"https://tusociapp.com/api/v1/expenses",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}", "Idempotency-Key": "gasto-0001"},
json={
"description": "Pago de luz",
"category": "servicios",
"amount": "1500.00",
"currency": "VES",
"method": "pago_movil",
"reference": "004321"
},
)
print(res.status_code, res.json()){
"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.
/sedes· Listar las sedescurl "https://tusociapp.com/api/v1/sedes" \
-H "Authorization: Bearer $SOCIA_KEY"const res = await fetch("https://tusociapp.com/api/v1/sedes", {
headers: { Authorization: `Bearer ${process.env.SOCIA_KEY}` },
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.get(
"https://tusociapp.com/api/v1/sedes",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
)
print(res.status_code, res.json())/products/{id}/stock· Existencias de un producto en cada sedeCada 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"const res = await fetch("https://tusociapp.com/api/v1/products/ID_DEL_PRODUCTO/stock", {
headers: { Authorization: `Bearer ${process.env.SOCIA_KEY}` },
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.get(
"https://tusociapp.com/api/v1/products/ID_DEL_PRODUCTO/stock",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
)
print(res.status_code, res.json())/transfers· Listar trasladoscurl "https://tusociapp.com/api/v1/transfers?limit=20" \
-H "Authorization: Bearer $SOCIA_KEY"const res = await fetch("https://tusociapp.com/api/v1/transfers?limit=20", {
headers: { Authorization: `Bearer ${process.env.SOCIA_KEY}` },
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.get(
"https://tusociapp.com/api/v1/transfers",
params={
"limit": 20
},
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
)
print(res.status_code, res.json())/transfers/{id}· Ver un trasladocurl "https://tusociapp.com/api/v1/transfers/ID_DEL_TRASLADO" \
-H "Authorization: Bearer $SOCIA_KEY"const res = await fetch("https://tusociapp.com/api/v1/transfers/ID_DEL_TRASLADO", {
headers: { Authorization: `Bearer ${process.env.SOCIA_KEY}` },
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.get(
"https://tusociapp.com/api/v1/transfers/ID_DEL_TRASLADO",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
)
print(res.status_code, res.json())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.
| Campo | Tipo | Qué es |
|---|---|---|
| customer_id o customer_name | texto | Quién se inscribe. Con un nombre nuevo, se crea el cliente. |
| plan_id | texto | El plan. Obligatorio. |
| periods | número | Cuántos períodos del plan paga de una vez. Por defecto 1. |
| phone, email, reminders | texto, sí/no | Su contacto y si aceptó recordatorios por WhatsApp. |
| method, reference, date | texto, día | Cómo y cuándo pagó. Con on_credit queda debiendo; con charge en false no se cobra. |
- 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.
/memberships· ListarFiltros: 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"const res = await fetch("https://tusociapp.com/api/v1/memberships?status=expiring&expiring_within_days=7", {
headers: { Authorization: `Bearer ${process.env.SOCIA_KEY}` },
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.get(
"https://tusociapp.com/api/v1/memberships",
params={
"status": "expiring",
"expiring_within_days": "7"
},
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
)
print(res.status_code, res.json())/memberships/search· Buscar por teléfono o correocurl "https://tusociapp.com/api/v1/memberships/search" \
-H "Authorization: Bearer $SOCIA_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+584140000000"
}'const res = await fetch("https://tusociapp.com/api/v1/memberships/search", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SOCIA_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"phone": "+584140000000"
}),
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.post(
"https://tusociapp.com/api/v1/memberships/search",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
json={
"phone": "+584140000000"
},
)
print(res.status_code, res.json())/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"
}'const res = await fetch("https://tusociapp.com/api/v1/memberships/check", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SOCIA_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"phone": "+584140000000"
}),
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.post(
"https://tusociapp.com/api/v1/memberships/check",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
json={
"phone": "+584140000000"
},
)
print(res.status_code, res.json())/memberships/{id}· Ver unacurl "https://tusociapp.com/api/v1/memberships/ID_DE_LA_MEMBRESIA" \
-H "Authorization: Bearer $SOCIA_KEY"const res = await fetch("https://tusociapp.com/api/v1/memberships/ID_DE_LA_MEMBRESIA", {
headers: { Authorization: `Bearer ${process.env.SOCIA_KEY}` },
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.get(
"https://tusociapp.com/api/v1/memberships/ID_DE_LA_MEMBRESIA",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
)
print(res.status_code, res.json())/memberships· Inscribircurl "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"
}'const res = await fetch("https://tusociapp.com/api/v1/memberships", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SOCIA_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "inscripcion-0001",
},
body: JSON.stringify({
"customer_name": "Ana Ejemplo",
"plan_id": "ID_DEL_PLAN",
"periods": 1,
"method": "pago_movil"
}),
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.post(
"https://tusociapp.com/api/v1/memberships",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}", "Idempotency-Key": "inscripcion-0001"},
json={
"customer_name": "Ana Ejemplo",
"plan_id": "ID_DEL_PLAN",
"periods": 1,
"method": "pago_movil"
},
)
print(res.status_code, res.json())/memberships/{id}/renew· RenovarSuma 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"
}'const res = await fetch("https://tusociapp.com/api/v1/memberships/ID_DE_LA_MEMBRESIA/renew", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SOCIA_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "renovacion-0001",
},
body: JSON.stringify({
"periods": 3,
"method": "zelle"
}),
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.post(
"https://tusociapp.com/api/v1/memberships/ID_DE_LA_MEMBRESIA/renew",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}", "Idempotency-Key": "renovacion-0001"},
json={
"periods": 3,
"method": "zelle"
},
)
print(res.status_code, res.json())/memberships/{id}/pause· PausarLos 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
}'const res = await fetch("https://tusociapp.com/api/v1/memberships/ID_DE_LA_MEMBRESIA/pause", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SOCIA_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "pausa-0001",
},
body: JSON.stringify({
"days": 14
}),
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.post(
"https://tusociapp.com/api/v1/memberships/ID_DE_LA_MEMBRESIA/pause",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}", "Idempotency-Key": "pausa-0001"},
json={
"days": 14
},
)
print(res.status_code, res.json())/memberships/{id}/cancel· Cancelarcurl "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 '{}'const res = await fetch("https://tusociapp.com/api/v1/memberships/ID_DE_LA_MEMBRESIA/cancel", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SOCIA_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "cancelar-0001",
},
body: JSON.stringify({}),
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.post(
"https://tusociapp.com/api/v1/memberships/ID_DE_LA_MEMBRESIA/cancel",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}", "Idempotency-Key": "cancelar-0001"},
json={},
)
print(res.status_code, res.json())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.
/memberships/{id}/payment-link· Crear un enlaceResponde { 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
}'const res = await fetch("https://tusociapp.com/api/v1/memberships/ID_DE_LA_MEMBRESIA/payment-link", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SOCIA_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "enlace-0001",
},
body: JSON.stringify({
"periods": 1
}),
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.post(
"https://tusociapp.com/api/v1/memberships/ID_DE_LA_MEMBRESIA/payment-link",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}", "Idempotency-Key": "enlace-0001"},
json={
"periods": 1
},
)
print(res.status_code, res.json())/payment-requests· Listar cobrosFiltros: status, customer_id y membership_id.
curl "https://tusociapp.com/api/v1/payment-requests?status=reported" \
-H "Authorization: Bearer $SOCIA_KEY"const res = await fetch("https://tusociapp.com/api/v1/payment-requests?status=reported", {
headers: { Authorization: `Bearer ${process.env.SOCIA_KEY}` },
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.get(
"https://tusociapp.com/api/v1/payment-requests",
params={
"status": "reported"
},
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
)
print(res.status_code, res.json())/payment-requests/{id}· Ver un cobrocurl "https://tusociapp.com/api/v1/payment-requests/ID_DEL_COBRO" \
-H "Authorization: Bearer $SOCIA_KEY"const res = await fetch("https://tusociapp.com/api/v1/payment-requests/ID_DEL_COBRO", {
headers: { Authorization: `Bearer ${process.env.SOCIA_KEY}` },
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.get(
"https://tusociapp.com/api/v1/payment-requests/ID_DEL_COBRO",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
)
print(res.status_code, res.json())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.
/orders· Listar pedidosPaginados como las ventas. Filtro: status.
curl "https://tusociapp.com/api/v1/orders?status=new" \
-H "Authorization: Bearer $SOCIA_KEY"const res = await fetch("https://tusociapp.com/api/v1/orders?status=new", {
headers: { Authorization: `Bearer ${process.env.SOCIA_KEY}` },
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.get(
"https://tusociapp.com/api/v1/orders",
params={
"status": "new"
},
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
)
print(res.status_code, res.json())/orders/{id}· Ver un pedidocurl "https://tusociapp.com/api/v1/orders/ID_DEL_PEDIDO" \
-H "Authorization: Bearer $SOCIA_KEY"const res = await fetch("https://tusociapp.com/api/v1/orders/ID_DEL_PEDIDO", {
headers: { Authorization: `Bearer ${process.env.SOCIA_KEY}` },
});
const data = await res.json();
console.log(res.status, data);import os
import requests
res = requests.get(
"https://tusociapp.com/api/v1/orders/ID_DEL_PEDIDO",
headers={"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"},
)
print(res.status_code, res.json())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.
{
"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"let cursor = null;
const customers = [];
do {
const url = new URL("https://tusociapp.com/api/v1/customers");
url.searchParams.set("limit", "100");
url.searchParams.set("updated_since", "2026-10-01T00:00:00Z");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.SOCIA_KEY}` } });
const page = await res.json();
customers.push(...page.data);
cursor = page.next_cursor;
} while (cursor);
console.log(customers.length);import os
import requests
headers = {"Authorization": f"Bearer {os.environ['SOCIA_KEY']}"}
params = {"limit": 100, "updated_since": "2026-10-01T00:00:00Z"}
customers = []
while True:
page = requests.get("https://tusociapp.com/api/v1/customers", params=params, headers=headers).json()
customers += page["data"]
if not page["has_more"]:
break
params["cursor"] = page["next_cursor"]
print(len(customers))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"
}
}| 400 | invalid_request, invalid_json, invalid_cursor | Falta un dato o está mal escrito. `field` dice cuál. |
| 401 | unauthenticated, invalid_key | No mandaste la clave, o no existe o fue revocada. |
| 402 | no_credits | Se acabaron los créditos: las escrituras se detienen; las lecturas siguen. |
| 403 | insufficient_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. |
| 404 | not_found | No existe en este negocio (o la ruta no existe). |
| 409 | idempotency_in_progress | La misma Idempotency-Key se está procesando todavía. |
| 422 | idempotency_mismatch | Usaste una Idempotency-Key que ya usaste con otros datos. |
| 429 | rate_limited, daily_limit | Demasiadas llamadas. Espera los segundos de Retry-After. |
| 500 | internal | Algo falló de nuestro lado y no se guardó nada. Ya nos llegó el aviso; intenta de nuevo. |
- 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: 9958Si 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
402conno_creditsy 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. |
- 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.
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
iddel 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);import hashlib
import hmac
import json
import os
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
SECRET = os.environ["SOCIA_WEBHOOK_SECRET"].encode()
def is_from_socia(header, body):
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
if not parts.get("t", "").isdigit():
return False
expected = hmac.new(SECRET, parts["t"].encode() + b"." + body, hashlib.sha256).hexdigest()
fresh = abs(time.time() - int(parts["t"])) < 300
return fresh and hmac.compare_digest(expected, parts.get("v1", ""))
class Hook(BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers["Content-Length"]))
if not is_from_socia(self.headers.get("X-SocIA-Signature", ""), body):
self.send_response(400)
self.end_headers()
return
event = json.loads(body)
print(event["type"], event["data"].get("id"))
self.send_response(200)
self.end_headers()
HTTPServer(("", 3000), Hook).serve_forever()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.