Supplier API: заявка на ручные заказы без лишней спешки
Опубликованная версия этого руководства советовала вам опрашивать GET /orders. Эта конечная точка отдаёт один и тот же заказ каждому опрашивающему, который вы запускаете. Существует конечная точка, которая атомарно захватывает заказ, аренда, которую вам нужно взять самостоятельно, и два лимита частоты запросов, о которых никто не документирует. Вот цикл, который действительно выдерживает нагрузку.
Sarah JohnsonЕсли вы выполняете ручные заказы через API, первое решение — какой эндпоинт забирает работу, и именно это решение большинство интеграций принимают неправильно. GET /orders — это чтение. Он возвращает подходящие заказы тому, кто спрашивает, и ничего не меняет. Запустите два воркера или один воркер с повтором — и оба получат один и тот же заказ, и оба попытаются его выполнить.
Эндпоинт, который забирает работу, — POST /orders-pull. Он выбирает ваши ожидающие ручные заказы, переключает каждый из статуса pending на processing с помощью compare-and-set и возвращает только те, чей переключатель действительно сработал. Если второй воркер спросит на миллисекунду позже, сравнение не пройдёт, и заказа в его ответе не будет. В описании эндпоинта указано, что он совместим с PerfectPanel — а это формат, который ожидает большинство панельных интеграций.
Цикл — в том порядке, в котором он должен выполняться
- Забрать.
POST /orders-pull. Необязательное телоlimit, по умолчанию 100, жёсткий максимум 500. Сначала самые старые заказы. - Аренда, если ваш воркер медленный.
PATCH /orders/{id}/last-process-timeбез тела фиксирует текущее время и скрывает заказ от вашего собственного опроса на тридцать минут. - Доставить.
POST /orders-updateсо строками учётных данных илиPOST /orders/{id}/accounts-bulkдля больших объёмов. - Закрыть. Тот же вызов
orders-updateнесётstatus: "completed"или"partial", если вы выполнили часть заказа.
Аутентификация — это заголовок в каждом вызове, и есть две принятые формы:
curl -X POST "https://api.hstockplus.com/api/admin/v2/orders-pull" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"limit": 50}'
# эквивалентно
curl -X POST "https://api.hstockplus.com/api/admin/v2/orders-pull" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"limit": 50}'
Сгенерируйте ключ на странице API Settings в вашей панели. Отсутствующий заголовок возвращает API_KEY_REQUIRED; неверный ключ возвращает INVALID_API_KEY. Это разные коды, и означают они разные баги.
Тридцатиминутное окно — это аренда, и она опциональна
Оба листинговых эндпоинта пропускают любой заказ, у которого lastProcessTime попадает в последние тридцать минут. Звучит как автоматическая дедупликация, но это не так, потому что ни один из эндпоинтов не записывает это поле. Никто его не пишет, кроме вас, через PATCH /orders/{id}/last-process-time.
Так что это поле — кооперативная аренда. Зафиксируйте его, когда забираете заказ, и он исчезнет из вашего следующего опроса на полчаса — это то, что нужно, если доставка занимает время, а ваш поллер запускается каждую минуту. Отправьте явную ISO-метку, чтобы задать другое время истечения, или null, чтобы освободить его немедленно. Игнорируйте эндпоинт целиком — и окно к вам никогда не применяется.
orders-pull уже защищает вас от двойного забора через переключение статуса. Аренда — для второго сценария отказа: ваш воркер взял заказ, всё ещё работает, и вы не хотите видеть его снова в это время.
Чтение заказов — с фильтрами по умолчанию, которые удивляют людей
GET /orders по-прежнему правильный эндпоинт для сверки, бэкфилла и дашбордов. Два его значения по умолчанию стоит знать, прежде чем строить на нём что-то.
- Статус оплаты по умолчанию — два значения, а не одно. Без параметра
paymentStatusвы получаете заказы со статусомcompletedилиpartial. Partial означает частично возвращённые, и такие заказы — всё ещё живая работа. Сам параметр принимает список через запятую: pending, completed, failed, refunded, processing и partial. - Тип продукта опционален и множественный. Для ручных заказов он не обязателен. Принимает
manual,inventoryиautoчерез запятую, так чтоproductType=manual,auto— валидно.
Фильтр статуса заказа принимает pending, processing, completed, refunded и error. partial среди них нет; это статус оплаты. Другие фильтры: entityType как product или smm_service, subCategory по имени, createdFrom и createdTo как ISO-моменты, limit до 500 и 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: YOUR_API_KEY"
Что происходит со строками учётных данных, которые вы отправляете
Вот откуда берётся большинство тикетов в поддержку по API, потому что перед сохранением выполняются три преобразования, и ни одно из них не видно в ответе.
- Каждый элемент разбивается по переводам строк. Одна запись массива может содержать целый файл.
["a:1\nb:2\nc:3"]и["a:1","b:2","c:3"]— это одна и та же отправка. Пустые строки и окружающие пробелы удаляются. - Повторы отбрасываются без учёта регистра. Внутри партии и против всего, что уже доставлено по этому заказу. Повторная отправка той же партии после таймаута безопасна и ничего не доставляет дважды.
- Лишние строки сверх количества заказа молча отбрасываются. Отправка усекается до оставшихся слотов. Ошибки не возникает, так что отправляйте правильное количество.
Что содержит строка — полностью ваше дело. Платформа хранит её как одну непрозрачную строку и никогда не парсит, так что пара через двоеточие, тройка через вертикальную черту с секретом двухфакторной аутентификации, токен, сессионный блоб — всё это просто текст. Какая бы конвенция ни была обещана в описании листинга, именно её будет ждать покупатель, и ничто не обеспечивает её соблюдение за вас.
Поскольку дубликаты против предыдущих доставок отбрасываются, а не отклоняются, частичная доставка через несколько вызовов — поддерживаемый паттерн. Отправьте, что есть, отметьте заказ как partial, отправьте остальное позже, затем отметьте как completed.
Два лимита частоты запросов, оба недокументированные до сих пор
orders-update несёт минимальный интервал на ключ API — 200 миллисекунд по умолчанию — и ограничение конкурентности на пользователя — два запроса в полёте. Превышение любого из них возвращает HTTP 429 с машиночитаемой задержкой:
{ "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 }
Читайте retry_after_ms и спите ровно столько. Фиксированный бэкофф будет либо слишком медленным для случая 200 мс, либо слишком быстрым для случая конкурентности. Есть также мьютекс на заказ: две доставки, соревнующиеся за один заказ, дают ORDER_DELIVERY_BUSY — тоже 429, и правильный ответ — короткий повтор, а не переотправка других учётных данных.
Коды ошибок, которые стоит обрабатывать отдельно
API_KEY_REQUIREDиINVALID_API_KEY: отсутствие заголовка против заголовка, который кэш не узнаёт.ORDER_NOT_FOUND: id заказа нет в кэше. Учтите, что обычный поставщик также может не увидеть только что созданный авто-заказ из-за короткой задержки видимости, так что 404 через секунды после создания — не обязательно постоянная ошибка.ACCESS_DENIED: заказ не содержит ни одной строки, принадлежащей вам.PAYMENT_NOT_COMPLETED: не оплачен или полностью возвращён. Доставлять в него нельзя, и повторять не стоит.INVALID_ACCOUNTS, сообщение «No valid accounts provided»: массив схлопнулся в ничто. Обычно это пустая строка или массив нестроковых значений.RATE_LIMIT,CONCURRENCY_LIMIT,ORDER_DELIVERY_BUSY: отступите и повторите идентичный запрос.
Два поля для вашего собственного учёта
orders-update принимает external_id для вашей внутренней ссылки на заказ, а также принимает supplierOrderId как алиас, если ваша панель уже отправляет в таком виде. Он принимает external_price — число, фиксирующее, сколько заказ стоил вам выше по цепочке. Эта цифра хранится для справки и не влияет на итоги платформы. Отправка null или пустой строки оставляет то, что уже есть, так что повторные обновления его не затирают.
Что на самом деле значит manual и насколько это быстро
Тип продукта manual означает, что поставщик загружает товары после поступления заказа. Это не настройка скорости доставки, и запись говорит об этом ясно. По всем выполненным ручным заказам на сайте — их 6 552 — медианное время от оплаты до завершения меньше тридцати секунд, три четверти укладываются в десять минут, и только примерно один из сорока занимает больше дня.
Инвентарные заказы практически мгновенны, потому что строки уже лежат на месте. Авто-заказы находятся между ними. Причина, по которой manual выглядит почти так же быстро, — именно в том, что поставщики с объёмами на нём запускают этот API с коротким опросом, а не смотрят на дашборд. Если ваша интеграция добавляет минуты — вы медленный хвост, а не норма.
Одни часы, которые стоит знать, если вы продаёте услуги роста, а не аккаунты: заказ на услугу роста получает ровно три дня послепродажного окна с момента доставки, независимо от того, что говорит листинг. Аккаунт-листинги несут собственную гарантию, заданную на каждый листинг.
Воркер, который ведёт себя правильно
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) {
// Заказ уже в статусе 'processing' и принадлежит вам. Возьмите аренду, если вы медленные.
await call('PATCH', '/orders/' + order.id + '/last-process-time', {});
const accounts = await prepareCredentials(order); // ваша система
await call('POST', '/orders-update', {
order: order.id,
status: accounts.length >= order.quantity ? 'completed' : 'partial',
accounts,
supplierOrderId: 'SUP-' + order.id,
});
}
}
Три вещи, которые этот цикл делает, а старая форма не делала: он забирает, а не читает; он уважает собственную задержку повтора сервера вместо того, чтобы угадывать её; и он честно сообщает partial, а не закрывает заказ, который не смог заполнить.
Полная справочная документация по эндпоинтам, включая создание продуктов, тикеты, возвраты и массовую задачу по аккаунтам, — на странице API для поставщиков. Если вы всё ещё решаете, должен ли листинг быть manual, inventory или auto, этот выбор описан в руководстве по управлению продуктами.



