Lee tu catálogo, tus existencias y el histórico de movimientos, registra entradas y salidas desde tu propio programa y recibe un aviso firmado en tu servidor cada vez que algo cambia. Todo en JSON, con una clave por integración.
Dirección base
https://trazza.newtex.es/api/v1
Autenticación
Authorization: Bearer trz_…
Plan
Enterprise (API y webhooks salientes)
Límite
120 peticiones por minuto y clave
Autenticación y plan
Cada petición lleva una clave de API en la cabecera Authorization: Bearer trz_…. Las claves se crean en Ajustes → Conexiones para desarrolladores; el secreto se enseña una sola vez al crearla (guardamos solo su huella, no la clave) y se puede revocar en cualquier momento. Usa una clave por integración: así puedes cortar una sin tocar las demás.
Todo lo que devuelve una clave es de su cuenta y solo de su cuenta: no hay forma de leer ni escribir datos de otra, y un identificador ajeno responde como si no existiera. Lo que registras por la API no se atribuye a ninguna persona: el operario queda vacío y el registro de actividad dice qué clave lo hizo.
La API y los webhooks salientes son del plan Enterprise. Una clave válida de una cuenta en otro plan recibe un 403 con details.code: PLAN_REQUIRED; es un 403 a propósito, para que tu programa pare en lugar de reintentar. Al bajar de plan las claves se revocan.
json
{
"error": "La función claves de API y webhooks es exclusiva del plan Enterprise. Contacta con NEWTEX para ampliar tu plan.",
"code": "FORBIDDEN",
"details": { "code": "PLAN_REQUIRED", "plan": "PRO", "feature": "apiAccess", "requiredPlan": "ENTERPRISE" },
"requestId": "…"
}
Límites, errores y CORS
Límite de peticiones
120 peticiones por minuto por clave. Cada respuesta correcta lleva X-RateLimit-Limit y X-RateLimit-Remaining; al pasarte recibes un 429 con Retry-After (segundos que faltan). Para traer mucho histórico, pagina con limit=100 en lugar de lanzar peticiones en paralelo.
Formato de error
Todos los errores son JSON con el mensaje en castellano, un code estable para tu código, un details opcional y el requestId, que también va en la cabecera X-Request-Id de toda respuesta. Si nos escribes por un fallo, ese identificador nos lleva directos a tu petición. Puedes mandar tu propio X-Request-Id y lo respetamos.
json
{
"error": "Datos de entrada inválidos.",
"code": "VALIDATION_ERROR",
"details": {
"issues": [
{ "path": "limit", "message": "No puede pasar de 100.", "code": "too_big" }
]
},
"requestId": "6f1c2a8e-3b4d-4e5f-9a0b-1c2d3e4f5a6b"
}
Estado
code
Cuándo
400
VALIDATION_ERROR
Parámetros o cuerpo que no cumplen el esquema (details.issues dice qué campo) o una regla de negocio (details.code: INSUFFICIENT_STOCK, LOT_REQUIRED, WORK_ORDER_FALTA_TIPO…).
400
BAD_REQUEST
El cuerpo no es un JSON válido.
401
UNAUTHORIZED
Sin clave, clave inventada o revocada.
402
PAYMENT_REQUIRED
details.code TRIAL_EXPIRED: el periodo de prueba de la cuenta ha terminado.
403
FORBIDDEN
details.code PLAN_REQUIRED (la cuenta no está en Enterprise), TENANT_INACTIVE (cuenta desactivada) o MODULE_DISABLED (el módulo que pides no está activado).
404
NOT_FOUND
El producto, almacén u orden no existe en tu cuenta, o la ruta no existe en la API v1.
409
CONFLICT
Almacén o producto archivado.
429
RATE_LIMITED
Más de 120 peticiones por minuto con la misma clave. Respeta Retry-After.
500
INTERNAL_ERROR
Fallo nuestro. Reintenta con espera y, si se repite, mándanos el requestId.
400
code
VALIDATION_ERROR
Cuándo
Parámetros o cuerpo que no cumplen el esquema (details.issues dice qué campo) o una regla de negocio (details.code: INSUFFICIENT_STOCK, LOT_REQUIRED, WORK_ORDER_FALTA_TIPO…).
400
code
BAD_REQUEST
Cuándo
El cuerpo no es un JSON válido.
401
code
UNAUTHORIZED
Cuándo
Sin clave, clave inventada o revocada.
402
code
PAYMENT_REQUIRED
Cuándo
details.code TRIAL_EXPIRED: el periodo de prueba de la cuenta ha terminado.
403
code
FORBIDDEN
Cuándo
details.code PLAN_REQUIRED (la cuenta no está en Enterprise), TENANT_INACTIVE (cuenta desactivada) o MODULE_DISABLED (el módulo que pides no está activado).
404
code
NOT_FOUND
Cuándo
El producto, almacén u orden no existe en tu cuenta, o la ruta no existe en la API v1.
409
code
CONFLICT
Cuándo
Almacén o producto archivado.
429
code
RATE_LIMITED
Cuándo
Más de 120 peticiones por minuto con la misma clave. Respeta Retry-After.
500
code
INTERNAL_ERROR
Cuándo
Fallo nuestro. Reintenta con espera y, si se repite, mándanos el requestId.
Regla para reintentar: un 4xx dice que la petición está mal y repetirla igual no la arregla; un 429 o un 5xx sí se reintentan, con espera.
CORS
Las respuestas llevan Access-Control-Allow-Origin: * y cada ruta contesta la consulta previa OPTIONS admitiendo Authorization, Content-Type y X-Request-Id. Aun así, no pongas la clave en código que se descarga en un navegador público: quien la lea puede usarla. Llama a la API desde tu servidor o desde una aplicación interna.
Endpoints
Identificadores en formato cuid (cm1p3x7a2…), fechas en ISO 8601 UTC y cantidades enteras. Los listados paginan con page y limit y devuelven el mismo bloque pagination: page, limit, total, totalPages, hasNext y hasPrev.
GET/api/v1/products
Catálogo de productos, paginado.
Parámetro
Qué hace
page
Página, desde 1. Por defecto 1.
limit
Filas por página, de 1 a 500. Por defecto 20.
search
Texto en SKU, nombre o código de barras.
code
SKU o código de barras exacto (sin distinguir mayúsculas).
category, tag
Solo los de esa categoría o etiqueta.
lowStock
true: solo los que están en su mínimo o por debajo.
includeArchived, includeVariants
true para incluir archivados o variantes.
sort, order
Ordenar por name, sku, price, threshold o createdAt; asc o desc.
page
Qué hace
Página, desde 1. Por defecto 1.
limit
Qué hace
Filas por página, de 1 a 500. Por defecto 20.
search
Qué hace
Texto en SKU, nombre o código de barras.
code
Qué hace
SKU o código de barras exacto (sin distinguir mayúsculas).
category, tag
Qué hace
Solo los de esa categoría o etiqueta.
lowStock
Qué hace
true: solo los que están en su mínimo o por debajo.
includeArchived, includeVariants
Qué hace
true para incluir archivados o variantes.
sort, order
Qué hace
Ordenar por name, sku, price, threshold o createdAt; asc o desc.
Una fila por producto y almacén. warehouseId (opcional) limita a un almacén. valorTotal es la cantidad por el coste medio de las entradas del producto (o su precio si nunca se registró un coste).
direction dice si el movimiento sube (INCREASE) o baja (DECREASE) el stock; en un traslado es null. operator es el nombre de quien lo registró, o null si lo registró una clave de API. Una salida no guarda coste propio: unitCost y totalCost salen null; el material de una orden sí la valora (ver el siguiente endpoint). Para sincronizar, pide sort=asc con from igual a la fecha del último movimiento que ya tienes y descarta los id repetidos.
clientKey (un UUID que generas tú) hace el envío repetible: si la respuesta se pierde y reenvías lo mismo, recibes el movimiento ya registrado en vez de mover el stock dos veces. Sin stock suficiente responde 400 con details.code: INSUFFICIENT_STOCK, available y requested. Cada movimiento dispara el webhook stock.movement y, si deja el producto en su mínimo, stock.low.
GET/api/v1/work-orders/{ref}
El material de una orden de trabajo, con su coste.
Solo con el módulo Órdenes de trabajo activado. Devuelve lo cargado en la orden (salidas), lo comprado para ella (entradas de albarán con la orden puesta), el coste de cada línea, el estado del aviso por correo de cada carga y los totales. {ref} se normaliza: 165pe, 165 PE y 0165-PE son la 165PE.
Una salida se valora a lo que costó la última compra del producto y se marca con estimatedCost: true; una entrada lleva su propio coste. Las compras de un albarán anulado no cuentan. loads[].status: OPEN (cargando), PENDING (cerrada, aviso por salir), SENT, SKIPPED (sin correo de aviso configurado) o FAILED. Una orden sin ningún movimiento en tu cuenta responde 404 NOT_FOUND; con el módulo apagado, 403 y details.code: MODULE_DISABLED.
Webhooks
En Ajustes → Conexiones para desarrolladores das de alta la dirección de tu servidor y eliges los eventos. Cada evento sale como un POST JSON con el sobre { id, event, data, tenantId }.
Evento
Cuándo
data
stock.movement
Se registra un movimiento (desde la app, un albarán o la API).
En stock.movement, productTotalStock llega cuando el movimiento se registra de uno en uno (no en las entradas en lote ni en los albaranes).
Ejemplo: material cargado en una orden
Cuando un operario termina de cargar material en una orden (pulsa «Terminar carga» o pasan 5 minutos sin lecturas), sale un único evento con todo lo cargado, sumado por producto. operator es el nombre de quien lo cargó.
http
POST /webhooks/trazza HTTP/1.1
Content-Type: application/json
X-Trazza-Event: material.cargado_en_orden
X-Trazza-Delivery-Id: 9c1f4e7a2b8d40c6a5e3f1b2c7d8e9f0
X-Trazza-Timestamp: 1759219068
X-Trazza-Signature: 3f6b…(HMAC-SHA256 del cuerpo, en hex)
X-Trazza-Signature-256: t=1759219068,v1=8a2e…(HMAC-SHA256 de "t.cuerpo", en hex)
{
"id": "9c1f4e7a2b8d40c6a5e3f1b2c7d8e9f0",
"event": "material.cargado_en_orden",
"data": {
"loadId": "cm1q8y9z00005ab12gh56ij78",
"order": "165PE",
"orderNumber": "165",
"orderType": "PE",
"operator": "Nombre del operario",
"from": "2026-09-30T08:12:44.000Z",
"to": "2026-09-30T08:16:02.000Z",
"lines": [
{ "sku": "FIL-0412", "name": "Filtro de aceite", "quantity": 2 },
{ "sku": "TOR-M6-20", "name": "Tornillo M6 x 20", "quantity": 8 }
]
},
"tenantId": "cm1p1a0b20000qw81zz9y8x7w"
}
Cabeceras
Cabecera
Contenido
X-Trazza-Event
El evento, igual que event en el cuerpo.
X-Trazza-Delivery-Id
Identificador de la entrega, igual que id en el cuerpo. Es el mismo en todos sus reintentos.
X-Trazza-Timestamp
Segundos Unix de este intento. Cambia en cada reintento, y la firma con él.
X-Trazza-Signature-256
t=<timestamp>,v1=<HMAC-SHA256 en hex de «timestamp.cuerpo»>. La que debes comprobar.
X-Trazza-Signature
HMAC-SHA256 en hex del cuerpo solo. Se mantiene por compatibilidad; no protege contra reenvíos.
X-Trazza-Event
Contenido
El evento, igual que event en el cuerpo.
X-Trazza-Delivery-Id
Contenido
Identificador de la entrega, igual que id en el cuerpo. Es el mismo en todos sus reintentos.
X-Trazza-Timestamp
Contenido
Segundos Unix de este intento. Cambia en cada reintento, y la firma con él.
X-Trazza-Signature-256
Contenido
t=<timestamp>,v1=<HMAC-SHA256 en hex de «timestamp.cuerpo»>. La que debes comprobar.
X-Trazza-Signature
Contenido
HMAC-SHA256 en hex del cuerpo solo. Se mantiene por compatibilidad; no protege contra reenvíos.
Entrega y reintentos
Responde con un 2xx en menos de 8 segundos y procesa después: pasado ese tiempo el intento se da por fallido. Si tu servidor no responde, responde 408, 429 o un 5xx, lo volvemos a intentar solos con espera creciente; cualquier otro 4xx se da por rechazado y no se repite (repetirlo no cambiaría la respuesta).
Intento
Cuándo
1
En el momento del evento.
2
1 minuto después del fallo anterior.
3
5 minutos después.
4
15 minutos después.
5
1 hora después.
6
6 horas después.
7 (último)
24 horas después: en total, unas 31 horas de margen.
1
Cuándo
En el momento del evento.
2
Cuándo
1 minuto después del fallo anterior.
3
Cuándo
5 minutos después.
4
Cuándo
15 minutos después.
5
Cuándo
1 hora después.
6
Cuándo
6 horas después.
7 (último)
Cuándo
24 horas después: en total, unas 31 horas de margen.
Cada reintento lleva el mismoX-Trazza-Delivery-Id y el mismo cuerpo, con un X-Trazza-Timestamp y una firma X-Trazza-Signature-256 nuevos: tu comprobación de los 5 minutos sigue valiendo para un reintento que llega horas después. Si se agotan los intentos, la entrega queda como fallida en Ajustes, con su código y el motivo, y desde ahí se puede reenviar (otra vez con el mismo id). Avisamos a la administración de tu cuenta una vez, no por cada entrega.
Idempotencia
La entrega es al menos una vez: si tu 2xx se pierde por el camino, el mismo evento puede llegar dos veces. Guarda el id de cada evento procesado y, si vuelve a llegar, responde 2xx sin aplicarlo otra vez: es tu garantía de no imputar dos veces lo mismo. Si tu servidor estuvo caído más de lo que cubren los reintentos, recupera lo que falte con GET /movements o GET /work-orders/{ref}. Solo enviamos a direcciones públicas; no se siguen redirecciones.
Verificar la firma
Cada endpoint tiene su secreto (whsec_…), que ves en Ajustes. Para comprobar un envío: separa t y v1 de X-Trazza-Signature-256, rechaza si t se aleja más de 5 minutos de tu reloj (así un envío capturado no se puede repetir más tarde), calcula el HMAC-SHA256 de `${t}.${cuerpo}` con el secreto sobre el cuerpo crudo, tal cual llegó, y compáralo con v1 en tiempo constante.
Node.js (Express)
javascript
import crypto from "node:crypto";
import express from "express";
const SECRETO = process.env.TRAZZA_WEBHOOK_SECRET; // whsec_…
const VENTANA_SEGUNDOS = 300;
function firmaValida(cabecera, cuerpo) {
const partes = Object.fromEntries(
String(cabecera ?? "").split(",").map((p) => p.trim().split("=", 2)),
);
const t = partes.t;
if (!/^\d+$/.test(t ?? "") || !partes.v1) return false;
if (Math.abs(Date.now() / 1000 - Number(t)) > VENTANA_SEGUNDOS) return false;
const esperada = crypto.createHmac("sha256", SECRETO).update(`${t}.${cuerpo}`).digest();
const recibida = Buffer.from(partes.v1, "hex");
return recibida.length === esperada.length && crypto.timingSafeEqual(recibida, esperada);
}
const app = express();
// El cuerpo CRUDO: volver a serializar el JSON cambia los bytes y la firma no casa.
app.post("/webhooks/trazza", express.raw({ type: "application/json" }), async (req, res) => {
const cuerpo = req.body.toString("utf8");
if (!firmaValida(req.get("X-Trazza-Signature-256"), cuerpo)) return res.sendStatus(401);
const evento = JSON.parse(cuerpo);
if (await yaProcesado(evento.id)) return res.sendStatus(200); // idempotencia
await guardarComoProcesado(evento.id);
if (evento.event === "material.cargado_en_orden") {
// Imputar evento.data.lines a la orden evento.data.order en tu programa.
}
res.sendStatus(200);
});
import hashlib
import hmac
import time
def firma_valida(cabecera: str, cuerpo: bytes, secreto: str, ventana: int = 300) -> bool:
partes = dict(p.strip().split("=", 1) for p in cabecera.split(",") if "=" in p)
t, v1 = partes.get("t", ""), partes.get("v1", "")
if not t.isdigit() or abs(time.time() - int(t)) > ventana:
return False
esperada = hmac.new(secreto.encode(), t.encode() + b"." + cuerpo, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperada, v1)
Integración con tu programa de gestión
El caso típico de un taller: el material se escanea en el almacén contra la orden de trabajo, y el programa de gestión lo necesita en la orden para facturarlo.
De TRAZZA a tu programa
Suscribe material.cargado_en_orden. Cada carga cerrada llega con la orden y las líneas (SKU, nombre y cantidad): tu programa imputa ese material a la orden. Verifica la firma y guarda el id para no imputarlo dos veces.
De tu programa a TRAZZA
Consulta GET /work-orders/{ref} para ver el material de una orden con su coste antes de cerrarla, o GET /movements?order=165PE para el detalle. Para cuadrar el día, GET /movements?from=…&sort=asc.
Dar de alta órdenes, productos o tarifas en TRAZZA desde tu programa no forma parte de la API estándar: es trabajo a medida y se diseña con cada integrador. Escríbenos a info@newtex.es con lo que necesitas conectar.