Este guía construye los dos trabajos que más necesitan los proveedores:
- Sincronización nocturna: envíe productos y clientes desde su ERP a TradeCatalog.
- Poll de órdenes: cada pocos minutos, obtenga nuevas órdenes en su ERP y márquelas como reconocidas.
Necesita una clave de API de Lectura y escritura. Vea Autenticación.
Cómo se comporta la sincronización nocturnaEnlace a esta sección#
- Los productos se coinciden por SKU y los clientes por código de cuenta, por lo que ejecutar la sincronización dos veces es inofensivo.
- Los nuevos productos se ponen en marcha a su precio de lista inmediatamente.
- Los precios de lista modificados no se ponen en marcha. Se acumulan en cambios de precio en borrador. Cada mañana, alguien abre Cambios de precio en la aplicación, revisa la vista previa y las advertencias, elige una fecha y publica. A los clientes se les puede enviar por correo electrónico sus nuevos precios.
brand,category,manufacturerRefydescriptionse escriben como se envían, por lo que siempre debe enviar el producto completo.- Los
emailydiscountBpde los clientes se escriben como se envían también:nulllos borra.
Envíe solo lo que cambió
Si su ERP puede indicarle qué productos han cambiado desde la última ejecución, envíelos solamente. Menos solicitudes, y menos productos para escanear en cada borrador.
Sincronización nocturna (Node.js)Enlace a esta sección#
Necesita Node.js 20 o posterior, para el fetch integrado. Reemplace las dos funciones fromErp con lecturas de su sistema.
// sync.mjs — run nightly: TRADECATALOG_API_KEY=tc_live_… node sync.mjs
const BASE = 'https://tradecatalog.app/api/v1'
const KEY = process.env.TRADECATALOG_API_KEY
const sleep = (seconds) => new Promise((r) => setTimeout(r, seconds * 1000))
async function api(method, path, body, attempt = 0) {
const res = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${KEY}`,
'Content-Type': 'application/json',
},
body: body && JSON.stringify(body),
})
if ((res.status === 429 || res.status >= 500) && attempt < 5) {
await sleep(Number(res.headers.get('Retry-After')) || 2 ** attempt)
return api(method, path, body, attempt + 1)
}
const json = await res.json()
if (!res.ok) {
const detail = json.error.issues
?.map((i) => `${i.path}: ${i.message}`)
.join('; ')
throw new Error(`${method} ${path}: ${json.error.message} ${detail ?? ''}`)
}
return json
}
// Replace these with reads from your ERP. Money in pence, discounts in basis points.
async function productsFromErp() {
return [
{
sku: 'HX-8510',
name: 'Hex bolt M10 x 50 zinc',
listPrice: 1950,
unitLabel: 'Box of 100',
brand: 'Northgate',
category: 'Bolts',
availability: 'in_stock',
},
]
}
async function customersFromErp() {
return [
{
accountCode: 'OAK01',
name: 'Oakfield Joinery',
email: 'accounts@oakfieldjoinery.co.uk',
discountBp: 500,
},
]
}
const batches = (rows, size = 100) =>
Array.from({ length: Math.ceil(rows.length / size) }, (_, i) =>
rows.slice(i * size, i * size + size),
)
const { data: workspace } = await api('GET', '/workspace')
console.log(`Syncing into ${workspace.name} (${workspace.currency})`)
for (const customers of batches(await customersFromErp())) {
const { data } = await api('PUT', '/customers', { customers })
console.log(`Customers: ${data.created} new, ${data.updated} updated`)
}
// Every batch adds its price changes to the same draft.
let priceChangeId
for (const products of batches(await productsFromErp())) {
const { data } = await api('PUT', '/products', { products, priceChangeId })
console.log(`Products: ${data.created} new, ${data.updated} updated`)
priceChangeId = data.priceChangeId ?? priceChangeId
if (data.unknownCustomers.length)
console.warn('Unknown customers in onlyFor:', data.unknownCustomers)
}
if (priceChangeId) {
const { data } = await api('GET', `/price-changes/${priceChangeId}`)
console.log(
`Draft price change: ${data.counts.up} up, ${data.counts.down} down,` +
` ${data.warnings.length} warnings. Publish it in TradeCatalog → Price changes.`,
)
}
Los clientes van primero, por lo que los productos con onlyFor pueden encontrarlos.
Ejúcelo desde cron, una tarea programada o el ejecutor de trabajos de su ERP:
0 2 * * * cd /opt/tradecatalog && TRADECATALOG_API_KEY=tc_live_… node sync.mjs >> sync.log 2>&1
Poll de órdenes (Node.js)Enlace a esta sección#
Cada pocos minutos, obtenga nuevas órdenes, páselas a su ERP y márquelas como reconocidas. El comprador recibirá un correo electrónico indicando que su orden ha sido reconocida.
// orders.mjs — run every 5 minutes. Copy BASE, KEY, sleep and api() from sync.mjs.
async function sendToErp(order) {
// Create the sales order in your system. Use order.reference as its external
// reference, and check it first so a retry can't create it twice.
console.log(
`${order.reference} for ${order.customer.accountCode}: ${order.lines.length} lines`,
)
}
const { data: newOrders } = await api('GET', '/orders?status=new')
for (const summary of newOrders.reverse()) {
const { data: order } = await api('GET', `/orders/${summary.id}`)
await sendToErp(order)
await api('PATCH', `/orders/${order.id}`, { status: 'acknowledged' })
}
Las órdenes más antiguas se manejan primero (reverse()), porque la lista está en orden de más reciente a más antiguo.
Si el paso de su ERP tiene éxito pero el PATCH falla, la orden sigue siendo new la próxima vez. Por eso sendToErp debe verificar si hay order.reference antes de crear algo.
Más adelante, cuando se envíe una orden, márquela como enviada con los detalles de seguimiento en la nota:
await api('PATCH', `/orders/${orderId}`, {
status: 'dispatched',
note: 'DPD tracking 1554 2210 0931',
})
Poll de órdenes (Python)Enlace a esta sección#
El mismo poller con requests:
# orders.py: run every 5 minutes. TRADECATALOG_API_KEY=tc_live_… python orders.py
import os
import time
import requests
BASE = "https://tradecatalog.app/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['TRADECATALOG_API_KEY']}"
def api(method, path, body=None, attempt=0):
res = session.request(method, BASE + path, json=body, timeout=30)
if (res.status_code == 429 or res.status_code >= 500) and attempt < 5:
time.sleep(int(res.headers.get("Retry-After", 2**attempt)))
return api(method, path, body, attempt + 1)
data = res.json()
if not res.ok:
raise RuntimeError(f"{method} {path}: {data['error']['message']}")
return data
def send_to_erp(order):
# Create the sales order in your system; skip it if order["reference"] exists.
print(order["reference"], order["customer"]["accountCode"], len(order["lines"]), "lines")
for summary in reversed(api("GET", "/orders?status=new")["data"]):
order = api("GET", f"/orders/{summary['id']}")["data"]
send_to_erp(order)
api("PATCH", f"/orders/{order['id']}", {"status": "acknowledged"})
Dinero y códigosEnlace a esta sección#
listPrice,unitPrice,lineTotalytotalestán en unidades menores (pence para GBP). Divídalos por 100 para obtener libras.- Los precios de las órdenes son como los estableció: antes del impuesto si su
taxLabeldice “ex …” (su ERP agrega IVA cuando emite la factura), incluyendo impuesto si dice “inc …”. - Use los códigos de cuenta de cliente de su ERP como códigos de cuenta de TradeCatalog, para que ambos sistemas coincidan sin necesidad de una tabla de búsqueda.
Límites por planEnlace a esta sección#
- 100 filas por
PUT, y 120 solicitudes por minuto por clave. Un catálogo de 10.000 productos es 100 solicitudes, aproximadamente un minuto. GET /orderscubre las 200 órdenes más recientes que coinciden. Consulte con suficiente frecuencia para que menos de 200 órdenes nuevas lleguen entre ejecuciones.- La publicación de cambios de precio, reglas de descuento y precios acordados se realiza en la aplicación.