Espina Comercial Developers
API pública · versión estable

Conectá cualquier sistema con Espina Comercial.

Consultá la operación de un negocio sin depender del frontend: productos, stock, ventas, balances, alertas y reportes. También podés reaccionar a cambios en tiempo real mediante webhooks.

URL base
https://www.espinacomercial.com
Formato
JSON · UTF-8
Autenticación
OAuth 2.1 + PKCE
Versión
/api/v1
01

Inicio rápido

De cero a una respuesta real

El acceso siempre representa a un usuario de Espina. Ese usuario decide qué permisos concede y la API sólo devuelve sus negocios activos.

  1. 1 Creá la aplicación

    Registrá el sistema externo y sus URLs de retorno desde tu cuenta.

  2. 2 Iniciá OAuth

    Redirigí al usuario al login de Espina usando Authorization Code + PKCE.

  3. 3 Obtené el token

    Canjeá el código temporal por un access token y un refresh token.

  4. 4 Elegí el negocio

    Consultá /me o /accounts para obtener el accountId.

  5. 5 Consumí recursos

    Usá el token Bearer para consultar productos, ventas, reportes o webhooks.

02

Autoservicio

Creá una integración desde tu cuenta

No necesitás solicitar acceso manual. El dueño del negocio registra la aplicación y Espina genera las credenciales.

i
¿Por qué se registra la URL de retorno?

OAuth exige comparar exactamente la URL enviada con una URL autorizada. Esto impide que otra página reciba el código de acceso. Espina no decide esa URL: la configura el propio usuario.

Aplicación con backend

Para servidores, automatizaciones y paneles privados capaces de guardar un secreto.

  • Recibe client_id y client_secret.
  • El canje del código usa autenticación HTTP Basic.
  • El secreto nunca debe enviarse al navegador.

Aplicación pública

Para SPA, aplicaciones de escritorio o móviles donde no es posible ocultar un secreto.

  • Recibe sólo un client_id.
  • PKCE es obligatorio.
  • El canje envía client_id, sin secret.
Integraciones privadas del negocio

Creá credenciales para sistemas propios. Las aplicaciones oficiales multiusuario se registran una sola vez y sus usuarios no configuran callbacks.

Abrir panel
03

Autenticación

OAuth 2.1 Authorization Code con PKCE

Los tokens pertenecen siempre al usuario que inicia sesión. Una aplicación oficial utiliza las mismas credenciales globales para todos, pero cada persona recibe tokens separados y sólo puede consultar sus propios negocios.

GET https://www.espinacomercial.com/oauth2/authorize

1. Generá PKCE

Creá un code_verifier aleatorio por intento y enviá su hash SHA-256 como code_challenge.

JavaScript
const bytes = crypto.getRandomValues(new Uint8Array(32));
const verifier = base64url(bytes);
const digest = await crypto.subtle.digest(
  "SHA-256",
  new TextEncoder().encode(verifier)
);
const challenge = base64url(new Uint8Array(digest));

function base64url(value) {
  return btoa(String.fromCharCode(...value))
    .replaceAll("+", "-")
    .replaceAll("/", "_")
    .replaceAll("=", "");
}

2. Redirigí al usuario

URL de autorización
https://www.espinacomercial.com/oauth2/authorize?
  response_type=code&
  client_id=TU_CLIENT_ID&
  redirect_uri=https%3A%2F%2Ftu-app.com%2Foauth%2Fcallback&
  scope=openid%20profile%20email%20accounts%3Aread%20products%3Aread&
  state=VALOR_ALEATORIO_UNICO&
  code_challenge=TU_CODE_CHALLENGE&
  code_challenge_method=S256&
  prompt=login
!
Validá state y solicitá la identidad correcta

Guardá y compará siempre state. Usá prompt=login cuando cada conexión deba pedir nuevamente las credenciales de Espina y no reutilizar otra sesión abierta.

3. Canjeá el código

cURL · cliente confidencial
curl --request POST 'https://www.espinacomercial.com/oauth2/token' \
  --user 'TU_CLIENT_ID:TU_CLIENT_SECRET' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode 'code=CODIGO_RECIBIDO' \
  --data-urlencode 'redirect_uri=https://tu-app.com/oauth/callback' \
  --data-urlencode 'code_verifier=TU_CODE_VERIFIER'
cURL · cliente público
curl --request POST 'https://www.espinacomercial.com/oauth2/token' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode 'client_id=TU_CLIENT_ID' \
  --data-urlencode 'code=CODIGO_RECIBIDO' \
  --data-urlencode 'redirect_uri=https://tu-app.com/oauth/callback' \
  --data-urlencode 'code_verifier=TU_CODE_VERIFIER'

Respuesta 200 OK

application/json
{
  "access_token": "eyJraWQiOiI...",
  "refresh_token": "pX7w2c...",
  "scope": "openid profile email accounts:read products:read",
  "id_token": "eyJraWQiOiI...",
  "token_type": "Bearer",
  "expires_in": 899
}

4. Renovación y revocación

El access token dura 15 minutos. El refresh token dura hasta 30 días y rota en cada uso: guardá el nuevo y descartá el anterior.

Renovar token
curl --request POST 'https://www.espinacomercial.com/oauth2/token' \
  --user 'TU_CLIENT_ID:TU_CLIENT_SECRET' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=refresh_token' \
  --data-urlencode 'refresh_token=TU_REFRESH_TOKEN'

Para revocar un token, enviá el mismo formato a POST /oauth2/revoke con el parámetro token.

04

Primera consulta

Obtené al usuario y sus negocios

Todos los endpoints protegidos reciben el access token en el header Authorization.

cURL
curl 'https://www.espinacomercial.com/api/v1/me' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer TU_ACCESS_TOKEN'
JavaScript · fetch
const response = await fetch('https://www.espinacomercial.com/api/v1/me', {
  headers: {
    Accept: 'application/json',
    Authorization: 'Bearer ' + accessToken
  }
});

if (!response.ok) throw await response.json();
const user = await response.json();
Python · requests
import requests

response = requests.get(
    'https://www.espinacomercial.com/api/v1/me',
    headers={
        'Accept': 'application/json',
        'Authorization': f'Bearer {access_token}',
    },
    timeout=15,
)
response.raise_for_status()
user = response.json()

Respuesta 200 OK

Usuario y negocios habilitados
{
  "id": 42,
  "name": "Gabriel Espina",
  "email": "gabriel@example.com",
  "pictureUrl": "https://...",
  "accounts": [
    {
      "id": 17,
      "name": "Kiosco Jardín 2",
      "role": "OWNER",
      "plan": "PRO",
      "subscriptionStatus": "ACTIVE",
      "trialEndsAt": null,
      "paidUntil": "2026-08-31",
      "active": true,
      "setupCompleted": true
    }
  ]
}

Modelo de acceso

Negocios y accountId

Un usuario puede administrar uno o varios negocios. Primero consultá GET /api/v1/accounts, elegí uno y colocá su id en las rutas que contienen {accountId}.

Aislamiento obligatorio

Aunque una aplicación modifique manualmente el ID, Espina valida que el usuario tenga una membresía activa. Un token nunca permite leer negocios ajenos.

Scopes

Pedí solamente los permisos necesarios

ScopePermite
accounts:readIdentidad, listado de negocios, estado y resumen operativo.
products:readProductos, precios, categorías y estado de stock.
inventory:readAlertas y movimientos de inventario.
sales:readVentas, detalle, totales y balances.
sales:writeRegistrar ventas de productos y ventas manuales.
reports:readBitácora operativa, series, métricas comerciales y reportes analíticos.
webhooks:manageCrear, modificar, probar y revisar webhooks.

Para OpenID Connect también podés solicitar openid profile email.

Convenciones

Filtros, fechas y paginación

Páginas

page comienza en 0. size acepta de 1 a 100 elementos.

Fechas

Usá ISO YYYY-MM-DD. El rango máximo por consulta es 366 días.

Importes

Se devuelven como números decimales y los balances informan ARS.

Enumeraciones

Estados y tipos se devuelven en mayúsculas, por ejemplo ACTIVE.

Formato paginado

ApiPage<T>
{
  "items": [],
  "page": 0,
  "size": 50,
  "totalItems": 134,
  "totalPages": 3,
  "first": true,
  "last": false
}

Referencia API

Identidad y negocios

Descubrí al usuario autenticado y los espacios que puede consultar.

accounts:read
GET/api/v1/me

Usuario actual y negocios accesibles.

Ejemplo
GET/api/v1/accounts

Lista los negocios del usuario.

GET/api/v1/accounts/{accountId}

Datos, plan, suscripción y rol.

GET/api/v1/accounts/{accountId}/status

Estado operativo, productos y miembros.

GET/api/v1/accounts/{accountId}/summary

Indicadores del panel de inicio.

Ejemplo de estado del negocio

GET …/status · 200 OK
{
  "accountId": 17,
  "operational": true,
  "accountStatus": "ACTIVE",
  "subscriptionStatus": "ACTIVE",
  "plan": "PRO",
  "activeProducts": 650,
  "lowStockProducts": 20,
  "members": 3,
  "generatedAt": "2026-07-29T18:25:41-03:00"
}

Referencia API

Productos e inventario

Consultá el catálogo actual, faltantes y trazabilidad de ingresos.

products:readinventory:read
GET/api/v1/accounts/{accountId}/products

Lista filtrable y paginada.

GET/api/v1/accounts/{accountId}/products/{productId}

Detalle de un producto.

GET/api/v1/accounts/{accountId}/products/categories

Categorías existentes.

GET/api/v1/accounts/{accountId}/inventory/alerts

Bajo stock y sin stock.

GET/api/v1/accounts/{accountId}/inventory/movements

Historial de movimientos.

Filtros de productos

search
Código o descripción.
category
Categoría exacta.
unit
Código o nombre de unidad.
active
true o false.
stockStatus
IN_STOCK, LOW_STOCK u OUT_OF_STOCK.
page / size
Paginación.
Consultar faltantes
curl 'https://www.espinacomercial.com/api/v1/accounts/17/products?stockStatus=LOW_STOCK&active=true&size=25' \
  --header 'Authorization: Bearer TU_ACCESS_TOKEN'

Producto

Elemento de items
{
  "id": 982,
  "code": "7790036974286",
  "description": "Agua saborizada naranja 500 ml",
  "category": "BEBIDAS",
  "unitCode": "UN",
  "unitName": "Unidad",
  "customUnit": null,
  "price": 1500.00,
  "currentStock": 4,
  "minimumStock": 6,
  "stockStatus": "LOW_STOCK",
  "active": true,
  "createdAt": "2026-02-12T11:42:19",
  "updatedAt": "2026-07-29T15:10:06"
}

Referencia API

Ventas y balances

Registrá operaciones y consultá detalle de productos, pagos y acumulados del período.

sales:readsales:writereports:read*
POST/api/v1/accounts/{accountId}/sales

Registra productos, valida stock y descuenta existencias.

POST/api/v1/accounts/{accountId}/sales/manual

Registra una venta manual sin productos.

GET/api/v1/accounts/{accountId}/sales

Ventas con filtros y paginación.

GET/api/v1/accounts/{accountId}/sales/{type}/{saleId}

Detalle de una venta.

GET/api/v1/accounts/{accountId}/sales/totals

Total, ticket promedio y medios de pago.

GET/api/v1/accounts/{accountId}/balances

Balance operativo completo.*

* /balances requiere simultáneamente sales:read y reports:read.

Filtros de ventas

from / to
Fechas ISO.
type
VENTA o MANUAL.
status
ACTIVA o ANULADA.
paymentMethod
EFECTIVO, TRANSFERENCIA o TARJETA.
seller
Búsqueda por vendedor.
minTotal / maxTotal
Rango de importe.
sort
date_desc, date_asc, total_desc o total_asc.

Totales del período

GET …/sales/totals · 200 OK
{
  "from": "2026-07-01",
  "to": "2026-07-29",
  "salesCount": 428,
  "grossSales": 5930240.00,
  "averageTicket": 13855.70,
  "paymentMethods": {
    "EFECTIVO": 2742800.00,
    "TRANSFERENCIA": 2079940.00,
    "TARJETA": 1107500.00
  }
}

Detalle de venta

GET …/sales/VENTA/845 · 200 OK
{
  "id": 845,
  "type": "VENTA",
  "status": "ACTIVA",
  "occurredAt": "2026-07-29T14:36:12",
  "seller": "Caja principal",
  "total": 4500.00,
  "observation": null,
  "payments": [
    { "method": "EFECTIVO", "amount": 3000.00 },
    { "method": "TRANSFERENCIA", "amount": 1500.00 }
  ],
  "lines": [
    {
      "productId": 982,
      "productCode": "7790036974286",
      "productDescription": "Agua saborizada naranja 500 ml",
      "quantity": 3,
      "unitPrice": 1500.00,
      "subtotal": 4500.00
    }
  ]
}

Referencia API

Reportes

La misma información analítica que utiliza el panel, disponible para tu propio BI o tablero.

reports:read
GET/api/v1/accounts/{accountId}/activity

Bitácora unificada con carga incremental.

GET/api/v1/accounts/{accountId}/reports

Reporte completo.

GET/api/v1/accounts/{accountId}/reports/summary

KPIs generales.

GET/api/v1/accounts/{accountId}/reports/sales-series

Serie temporal de ventas.

GET/api/v1/accounts/{accountId}/reports/products

Productos principales y accionables.

GET/api/v1/accounts/{accountId}/reports/categories

Comparación por categoría.

GET/api/v1/accounts/{accountId}/reports/payment-methods

Totales por medio de pago.

GET/api/v1/accounts/{accountId}/reports/cash

Caja y cobros digitales.

GET/api/v1/accounts/{accountId}/reports/inventory

Resumen y alertas de stock.

GET/api/v1/accounts/{accountId}/reports/sellers

Rendimiento por vendedor.

GET/api/v1/accounts/{accountId}/reports/peak-hours

Ventas agrupadas por hora.

Parámetros comunes

from / to
Período ISO; la bitácora admite hasta 31 días por consulta.
offset / size
Carga incremental de la bitácora; usá nextOffset mientras hasMore sea verdadero.
grouping
Agrupación temporal admitida por reportes: diaria, semanal o mensual.
Serie para un gráfico externo
curl 'https://www.espinacomercial.com/api/v1/accounts/17/reports/sales-series?from=2026-07-01&to=2026-07-29&grouping=DAILY' \
  --header 'Authorization: Bearer TU_ACCESS_TOKEN'

Referencia API

Administración de webhooks

Configurá destinos HTTPS y auditá cada intento de entrega.

webhooks:manage
GET/api/v1/accounts/{accountId}/webhooks/events

Eventos disponibles.

POST/api/v1/accounts/{accountId}/webhooks

Crea una suscripción.

GET/api/v1/accounts/{accountId}/webhooks

Lista suscripciones.

PATCH/api/v1/accounts/{accountId}/webhooks/{subscriptionId}

Actualiza o activa/desactiva.

DELETE/api/v1/accounts/{accountId}/webhooks/{subscriptionId}

Desactiva el destino.

POST/api/v1/accounts/{accountId}/webhooks/{subscriptionId}/rotate-secret

Rota la firma.

POST/api/v1/accounts/{accountId}/webhooks/{subscriptionId}/test

Encola un evento de prueba.

GET/api/v1/accounts/{accountId}/webhooks/{subscriptionId}/deliveries

Historial paginado.

POST/api/v1/accounts/{accountId}/webhooks/{subscriptionId}/deliveries/{deliveryId}/replay

Reencola una entrega.

Crear webhook
curl --request POST 'https://www.espinacomercial.com/api/v1/accounts/17/webhooks' \
  --header 'Authorization: Bearer TU_ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "name": "Alertas de stock",
    "targetUrl": "https://tu-app.com/webhooks/espina",
    "events": ["stock.low", "stock.out"]
  }'

Respuesta 201 Created

El signingSecret aparece una sola vez
{
  "id": 24,
  "accountId": 17,
  "name": "Alertas de stock",
  "targetUrl": "https://tu-app.com/webhooks/espina",
  "events": ["stock.low", "stock.out"],
  "active": true,
  "lastSuccessAt": null,
  "lastFailureAt": null,
  "createdAt": "2026-07-29T18:42:00",
  "signingSecret": "whsec_..."
}
05

Eventos en tiempo real

Recibí cambios mediante webhooks

Espina envía un POST JSON cuando ocurre un evento al que tu destino está suscripto.

sale.createdNueva venta.
sale.cancelledVenta anulada.
product.createdProducto creado.
product.updatedProducto actualizado.
product.deactivatedProducto desactivado.
product.deletedProducto eliminado.
inventory.changedMovimiento de stock.
stock.lowStock mínimo alcanzado.
stock.outProducto sin stock.
cash.closedCierre de caja.
subscription.status_changedCambio de suscripción.
webhook.testPrueba manual.

Headers de entrega

X-Espina-EventTipo de evento.
X-Espina-Event-IdID estable para deduplicación.
X-Espina-Delivery-IdID de este intento.
X-Espina-TimestampUnix timestamp utilizado para firmar.
X-Espina-SignatureFirma HMAC SHA-256 con prefijo v1=.
Payload de ejemplo
{
  "id": "01J4M6R7...",
  "type": "stock.low",
  "createdAt": "2026-07-29T21:42:00Z",
  "accountId": 17,
  "data": {
    "productId": 982,
    "code": "7790036974286",
    "description": "Agua saborizada naranja 500 ml",
    "currentStock": 4,
    "minimumStock": 6
  }
}

Seguridad de webhooks

Verificá la firma antes de procesar

La cadena firmada es timestamp + "." + rawBody. Usá el cuerpo sin parsear y una comparación de tiempo constante.

Node.js
import crypto from "node:crypto";

export function verifyEspinaWebhook(rawBody, headers, secret) {
  const timestamp = headers["x-espina-timestamp"];
  const received = headers["x-espina-signature"];
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));

  if (!timestamp || !received || age > 300) return false;

  const expected = "v1=" + crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  return expected.length === received.length &&
    crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(received)
    );
}

Entregas

Respondé rápido y deduplicá

  • Respondé cualquier estado 2xx después de guardar el evento.
  • Procesá tareas lentas en segundo plano.
  • Deduplicá por X-Espina-Event-Id; un evento puede entregarse más de una vez.
  • Los fallos se reintentan a 1 minuto, 5 minutos, 30 minutos, 2 horas y 12 horas.
  • Después quedan en DEAD y pueden reencolarse con el endpoint de replay.

Contrato HTTP

Errores predecibles

Los errores usan application/problem+json. Nunca dependas del texto: tomá decisiones con status y code.

EstadoCodeSignificado
400invalid_requestParámetro, fecha, filtro o body inválido.
401unauthorizedToken ausente, vencido o inválido.
403forbiddenFalta un scope.
403account_access_deniedEl usuario no pertenece al negocio.
404resource_not_foundRecurso inexistente dentro del negocio.
429rate_limit_exceededSe superó el límite temporal.
500internal_errorError interno; informá el requestId.
application/problem+json
{
  "type": "https://www.espinacomercial.com/problems/account_access_denied",
  "title": "Acceso al negocio denegado",
  "status": 403,
  "detail": "No tenés acceso al negocio solicitado.",
  "instance": "/api/v1/accounts/999/products",
  "code": "account_access_denied",
  "requestId": "req_01J4M8F5KD7",
  "timestamp": "2026-07-29T18:52:18-03:00"
}

Operación confiable

Límites, trazabilidad y CORS

120 requests/minuto

Por aplicación y usuario. Puede ajustarse según el entorno.

X-Request-Id

Espina genera uno o respeta el tuyo. Guardalo con cada log.

Headers de límite

X-RateLimit-Limit, Remaining y Reset.

CORS para integraciones

Los frontends HTTPS pueden usar la API con tokens Bearer; Espina no comparte cookies de sesión.

Listo para integrar

Creá tu aplicación y empezá con datos reales.

Las credenciales pertenecen a tu negocio y podés revocarlas cuando quieras.

Crear integración Explorar Swagger