API de proveedores: Reclamar pedidos manuales sin competir contra el reloj
La versión publicada de esta guía te decía que consultaras GET /orders. Ese endpoint le entrega el mismo pedido a cada poller que ejecutes. Existe un endpoint que reclama un pedido de forma atómica, un lease que tienes que tomar tú mismo y dos límites de tasa que nadie documenta. Aquí está el bucle que realmente aguanta bajo carga.
Sarah JohnsonSi cumples pedidos manuales a través de la API, la primera decisión es qué endpoint recoge el trabajo, y es la decisión que la mayoría de las integraciones hacen mal. GET /orders es una lectura. Devuelve los pedidos que coinciden a quien los pida y no cambia nada. Ejecuta dos workers, o un worker con reintento, y a ambos se les entregará el mismo pedido y ambos intentarán entregarlo.
El endpoint que reclama el trabajo es POST /orders-pull. Selecciona tus pedidos manuales pendientes, los cambia de pendiente a procesando con una operación de comparar y asignar, y devuelve solo aquellos cuyo cambio realmente se aplicó. Si un segundo worker pregunta un milisegundo después, la comparación falla y ese pedido no está en su respuesta. Su propia nota de origen lo describe como compatible con PerfectPanel, que es la forma que la mayoría de las integraciones de paneles ya esperan.
El bucle, en el orden en que debe ejecutarse
- Reclamar.
POST /orders-pull. Cuerpo opcionallimit, por defecto 100, con tope máximo de 500. El pedido más antiguo primero. - Arrendar, si tu worker es lento.
PATCH /orders/{id}/last-process-timesin cuerpo marca la hora actual, lo que oculta el pedido de tu propio sondeo durante treinta minutos. - Entregar.
POST /orders-updatecon las líneas de credenciales, oPOST /orders/{id}/accounts-bulkpara algo grande. - Cerrar. La misma llamada
orders-updatellevastatus: "completed", o"partial"si has completado solo una parte.
La autenticación es una cabecera en cada llamada, y hay dos formas aceptadas:
curl -X POST "https://api.hstockplus.com/api/admin/v2/orders-pull" \
-H "X-Api-Key: TU_CLAVE_API" \
-H "Content-Type: application/json" \
-d '{"limit": 50}'
# equivalente
curl -X POST "https://api.hstockplus.com/api/admin/v2/orders-pull" \
-H "Authorization: Bearer TU_CLAVE_API" \
-H "Content-Type: application/json" \
-d '{"limit": 50}'
Genera la clave en la página de Configuración de API de tu panel. Una cabecera ausente devuelve API_KEY_REQUIRED; una clave incorrecta devuelve INVALID_API_KEY. Son códigos diferentes y significan errores diferentes.
La ventana de treinta minutos es un arrendamiento, y es opcional
Ambos endpoints de listado omiten cualquier pedido cuyo lastProcessTime esté dentro de los últimos treinta minutos. Eso suena a deduplicación automática y no lo es, porque ninguno de los dos endpoints escribe el campo. Nada lo escribe excepto tú, a través de PATCH /orders/{id}/last-process-time.
Así que el campo es un arrendamiento cooperativo. Márcalo cuando recojas un pedido y desaparecerá de tu siguiente sondeo durante media hora, que es lo que quieres si la entrega lleva un tiempo y tu sondeo se ejecuta cada minuto. Envía una marca de tiempo ISO explícita para establecer una caducidad diferente, o null para liberarlo inmediatamente. Ignora el endpoint por completo y la ventana nunca se aplica a ti.
orders-pull ya te protege contra la doble reclamación mediante el cambio de estado. El arrendamiento es para el segundo modo de fallo: tu worker tomó el pedido, todavía está trabajando, y no quieres volver a verlo mientras tanto.
Leer pedidos, con los filtros por defecto que sorprenden a la gente
GET /orders sigue siendo el endpoint adecuado para conciliación, relleno y paneles. Dos de sus valores por defecto merece la pena conocer antes de construir sobre él.
- El estado de pago por defecto son dos valores, no uno. Sin el parámetro
paymentStatusobtienes pedidos que estáncompletedopartial. Parcial significa parcialmente reembolsado, y esos pedidos siguen siendo trabajo activo. El parámetro en sí acepta una lista separada por comas de pending, completed, failed, refunded, processing y partial. - El tipo de producto es opcional y plural. No es obligatorio para pedidos manuales. Acepta
manual,inventoryyauto, separados por comas, así queproductType=manual,autoes válido.
El filtro de estado del pedido acepta pending, processing, completed, refunded y error. partial no está entre ellos; es un estado de pago. Otros filtros: entityType como product o smm_service, subCategory por nombre, createdFrom y createdTo como instantes ISO, limit hasta 500, y offset.
curl -X GET "https://api.hstockplus.com/api/admin/v2/orders\
?status=pending,processing&productType=manual&paymentStatus=completed,partial&limit=50" \
-H "X-Api-Key: TU_CLAVE_API"
Qué ocurre con las líneas de credenciales que envías
Aquí es donde surgen la mayoría de los tickets de soporte sobre la API, porque se ejecutan tres transformaciones antes de que se almacene nada y ninguna de ellas es visible en la respuesta.
- Cada elemento se divide por saltos de línea. Una entrada de array puede contener un archivo completo.
["a:1\nb:2\nc:3"]y["a:1","b:2","c:3"]son la misma presentación. Las líneas en blanco y los espacios circundantes desaparecen. - Los repetidos se descartan, sin distinguir mayúsculas. Dentro del lote, y contra todo lo ya entregado en ese pedido. Reenviar el mismo lote después de un tiempo de espera es seguro y no entrega nada dos veces.
- Las líneas adicionales más allá de la cantidad del pedido se descartan silenciosamente. La presentación se trunca a los espacios restantes. No da error, así que envía la cantidad correcta.
Lo que contiene una línea depende completamente de ti. La plataforma la almacena como una sola cadena opaca y nunca la analiza, así que un par separado por dos puntos, un triple separado por barras con un secreto de dos factores, un token, un blob de sesión: todos son solo texto. La convención que prometa la descripción de tu listado es la convención que el comprador esperará, y nada la hace cumplir en tu nombre.
Dado que los duplicados contra entregas anteriores se descartan en lugar de rechazarse, la entrega parcial en varias llamadas es un patrón admitido. Envía lo que tengas, marca el pedido como partial, envía el resto más tarde, y luego márcalo como completed.
Dos límites de velocidad, ambos sin documentar hasta ahora
orders-update lleva un intervalo mínimo por clave API, 200 milisegundos por defecto, y un límite de concurrencia por usuario de dos solicitudes en vuelo. Superar cualquiera de los dos devuelve HTTP 429 con un retraso legible por máquina:
{ "error": "RATE_LIMIT", "message": "Too many order update requests",
"retry_after_ms": 137 }
{ "error": "CONCURRENCY_LIMIT", "message": "Too many concurrent sync requests",
"retry_after_ms": 5000 }
Lee retry_after_ms y espera ese tiempo. Una retroceso fijo será demasiado lento para el caso de 200 ms o demasiado rápido para el caso de concurrencia. También hay un mutex por pedido: dos entregas compitiendo en el mismo pedido dan ORDER_DELIVERY_BUSY, también un 429, y la respuesta correcta es un reintento corto en lugar de un reenvío de credenciales diferentes.
Códigos de error que vale la pena manejar por separado
API_KEY_REQUIREDyINVALID_API_KEY: sin cabecera versus una cabecera que la caché no reconoce.ORDER_NOT_FOUND: el id del pedido no está en la caché. Ten en cuenta que un proveedor normal también puede no ver nada para un pedido automático recién creado durante un breve retraso de visibilidad, así que un 404 segundos después de la creación no es necesariamente permanente.ACCESS_DENIED: el pedido no contiene ninguna línea que te pertenezca.PAYMENT_NOT_COMPLETED: no pagado o totalmente reembolsado. No puedes entregar en él y no deberías reintentar.INVALID_ACCOUNTS, mensaje "No valid accounts provided": el array se aplanó a nada. Normalmente una cadena vacía o un array de no cadenas.RATE_LIMIT,CONCURRENCY_LIMIT,ORDER_DELIVERY_BUSY: retrocede y reintenta la solicitud idéntica.
Dos campos para tu propia contabilidad
orders-update acepta external_id para tu referencia de pedido interna, y también acepta supplierOrderId como alias si esa es la forma que tu panel ya envía. Acepta external_price, un número que registra cuánto te costó el pedido aguas arriba. Esa cifra se almacena como referencia y no toca los totales de la plataforma. Enviar null o una cadena vacía deja lo que ya haya, así que las actualizaciones repetidas no lo borrarán.
Qué significa realmente manual, y cuán rápido es de verdad
El tipo de producto manual significa que el proveedor sube los productos después de que llegue el pedido. No es una configuración de velocidad de entrega, y el registro lo dice claramente. En todos los pedidos de productos manuales completados en el sitio, 6.552 de ellos, el tiempo medio desde el pago hasta la finalización es inferior a treinta segundos, tres cuartos terminan en diez minutos, y solo aproximadamente uno de cada cuarenta tarda más de un día.
Los pedidos de inventario son efectivamente instantáneos porque las filas ya están ahí. Los pedidos automáticos se sitúan entre los dos. La razón por la que lo manual parece casi tan rápido es precisamente que los proveedores que hacen volumen con ello están ejecutando esta API con un sondeo corto en lugar de mirar un panel. Si tu integración añade minutos, eres la cola lenta, no la norma.
Un reloj que conocer si vendes servicios de crecimiento en lugar de cuentas: un pedido de servicio de crecimiento tiene exactamente tres días de ventana postventa desde el momento en que se entrega, independientemente de lo que diga el listado. Los listados de cuentas llevan su propia garantía, establecida por listado.
Un worker que se comporta
const BASE = 'https://api.hstockplus.com/api/admin/v2';
const HEAD = { 'X-Api-Key': process.env.API_KEY, 'Content-Type': 'application/json' };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function call(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: HEAD,
body: body ? JSON.stringify(body) : undefined,
});
const json = await res.json().catch(() => ({}));
if (res.status === 429) {
await sleep(json.retry_after_ms ?? 1000);
return call(method, path, body);
}
if (!res.ok) throw Object.assign(new Error(json.message || res.status), { code: json.error });
return json;
}
async function tick() {
const { orders = [] } = await call('POST', '/orders-pull', { limit: 50 });
for (const order of orders) {
// El pedido ya está en 'processing' y es tuyo. Toma un arrendamiento si eres lento.
await call('PATCH', '/orders/' + order.id + '/last-process-time', {});
const accounts = await prepareCredentials(order); // tu sistema
await call('POST', '/orders-update', {
order: order.id,
status: accounts.length >= order.quantity ? 'completed' : 'partial',
accounts,
supplierOrderId: 'SUP-' + order.id,
});
}
}
Tres cosas que ese bucle hace que la forma anterior no hacía: reclama en lugar de leer, respeta el retraso de reintento del propio servidor en lugar de adivinar uno, e informa partial honestamente en lugar de cerrar un pedido que no pudo completar.
La referencia completa del endpoint, incluida la creación de productos, tickets, reembolsos y el trabajo de cuentas masivas, está en la página de API para proveedores. Si todavía estás decidiendo si un listado debería ser manual, inventario o automático en primer lugar, esa elección se cubre en la guía de gestión de productos para proveedores.
```


