API fournisseur : réclamer des commandes manuelles sans se précipiter
The published version of this guide told you to poll GET /orders. That endpoint hands the same order to every poller you run. There is an endpoint that claims an order atomically, a lease you have to take yourself, and two rate limits nobody documents. Here is the loop that actually holds under load.
Sarah JohnsonIf you fulfil manual orders through the API, the first decision is which endpoint collects the work, and it is the decision most integrations get wrong. GET /orders is a read. It returns matching orders to whoever asks and changes nothing. Run two workers, or one worker with a retry, and both will be handed the same order and both will try to deliver it.
The endpoint that claims work is POST /orders-pull. It selects your pending manual orders, flips each one from pending to processing with a compare and set, and returns only the ones whose flip actually landed. If a second worker asks a millisecond later, the compare fails and that order is not in its response. Its own source note describes it as PerfectPanel compatible, which is the shape most panel integrations already expect.
The loop, in the order it should run
- Claim.
POST /orders-pull. Optional bodylimit, default 100, hard capped at 500. Oldest order first. - Lease, if your worker is slow.
PATCH /orders/{id}/last-process-timewith no body stamps now, which hides the order from your own polling for thirty minutes. - Deliver.
POST /orders-updatewith the credential lines, orPOST /orders/{id}/accounts-bulkfor anything large. - Close. The same
orders-updatecall carriesstatus: "completed", or"partial"if you filled some of it.
Authentication is a header on every call, and there are two accepted forms:
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}'
# equivalent
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}'
Generate the key on the API Settings page in your dashboard. A missing header returns API_KEY_REQUIRED; a wrong key returns INVALID_API_KEY. Those are different codes and they mean different bugs.
The thirty minute window is a lease, and it is opt in
Both listing endpoints skip any order whose lastProcessTime is inside the last thirty minutes. That sounds like automatic deduplication and it is not, because neither endpoint writes the field. Nothing writes it except you, through PATCH /orders/{id}/last-process-time.
So the field is a cooperative lease. Stamp it when you pick an order up and it disappears from your next poll for half an hour, which is what you want if delivery takes a while and your poller runs every minute. Send an explicit ISO timestamp to set a different expiry, or null to release it immediately. Ignore the endpoint entirely and the window never applies to you.
orders-pull already protects you against double claiming through the status flip. The lease is for the second failure mode: your worker took the order, is still working, and you do not want to see it again in the meantime.
Reading orders, with the filter defaults that surprise people
GET /orders is still the right endpoint for reconciliation, backfill and dashboards. Two of its defaults are worth knowing before you build on it.
- Payment status defaults to two values, not one. With no
paymentStatusparameter you get orders that arecompletedorpartial. Partial means partially refunded, and those orders are still live work. The parameter itself accepts a comma separated list from pending, completed, failed, refunded, processing and partial. - Product type is optional and plural. It is not required for manual orders. It accepts
manual,inventoryandauto, comma separated, soproductType=manual,autois valid.
The order status filter takes pending, processing, completed, refunded and error. partial is not among them; it is a payment status. Other filters: entityType as product or smm_service, subCategory by name, createdFrom and createdTo as ISO instants, limit up to 500, and 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"
What happens to the credential lines you send
This is where most support tickets on the API come from, because three transformations run before anything is stored and none of them is visible in the response.
- Every element is split on newlines. One array entry can hold a whole file.
["a:1\nb:2\nc:3"]and["a:1","b:2","c:3"]are the same submission. Blank lines and surrounding whitespace go. - Repeats are dropped, case insensitively. Within the batch, and against everything already delivered on that order. Resending the same batch after a timeout is safe and delivers nothing twice.
- Extra lines beyond the order quantity are discarded silently. The submission is truncated to the remaining slots. It does not error, so send the right count.
What a line contains is entirely up to you. The platform stores it as one opaque string and never parses it, so a colon separated pair, a pipe separated triple with a two factor secret, a token, a session blob: all of them are just text. Whatever convention your listing description promises is the convention the buyer will expect, and nothing enforces it on your behalf.
Because duplicates against prior deliveries are dropped rather than rejected, partial delivery across several calls is a supported pattern. Send what you have, mark the order partial, send the rest later, then mark it completed.
Two rate limits, both undocumented until now
orders-update carries a minimum interval per API key, 200 milliseconds by default, and a per user concurrency cap of two requests in flight. Exceeding either returns HTTP 429 with a machine readable delay:
{ "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 }
Read retry_after_ms and sleep for it. A fixed backoff will either be too slow for the 200 ms case or too fast for the concurrency case. There is also a per order mutex: two deliveries racing on the same order give ORDER_DELIVERY_BUSY, also a 429, and the correct response is a short retry rather than a resend of different credentials.
Error codes worth handling separately
API_KEY_REQUIREDandINVALID_API_KEY: no header versus a header the cache does not recognise.ORDER_NOT_FOUND: the order id is not in cache. Note that a regular supplier can also be shown nothing for a newly created auto order for a short visibility delay, so a 404 seconds after creation is not necessarily a permanent one.ACCESS_DENIED: the order contains no line belonging to you.PAYMENT_NOT_COMPLETED: unpaid or fully refunded. You cannot deliver into it and you should not retry.INVALID_ACCOUNTS, message "No valid accounts provided": the array flattened to nothing. Usually an empty string or an array of non strings.RATE_LIMIT,CONCURRENCY_LIMIT,ORDER_DELIVERY_BUSY: back off and retry the identical request.
Two fields for your own bookkeeping
orders-update accepts external_id for your internal order reference, and it also accepts supplierOrderId as an alias if that is the shape your panel already sends. It accepts external_price, a number recording what the order cost you upstream. That figure is stored for reference and does not touch platform totals. Sending null or an empty string leaves whatever is already there, so repeat updates will not wipe it.
What manual actually means, and how fast it really is
Product type manual means the supplier uploads the goods after the order arrives. It is not a delivery speed setting, and the record says so clearly. Across every completed manual product order on the site, 6,552 of them, the median time from payment to completion is under thirty seconds, three quarters finish inside ten minutes, and only about one in forty takes longer than a day.
Inventory orders are effectively instantaneous because the rows are already sitting there. Auto orders sit between the two. The reason manual looks nearly as quick is precisely that the suppliers doing volume on it are running this API on a short poll rather than watching a dashboard. If your integration adds minutes, you are the slow tail, not the norm.
One clock to know if you sell growth services rather than accounts: a growth-service order gets exactly three days of after-sales window from the moment it is delivered, regardless of what the listing says. Account listings carry their own warranty instead, set per listing.
A worker that behaves
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) {
// The order is already 'processing' and is yours. Take a lease if you are slow.
await call('PATCH', '/orders/' + order.id + '/last-process-time', {});
const accounts = await prepareCredentials(order); // your system
await call('POST', '/orders-update', {
order: order.id,
status: accounts.length >= order.quantity ? 'completed' : 'partial',
accounts,
supplierOrderId: 'SUP-' + order.id,
});
}
}
Three things that loop does which the older shape did not: it claims rather than reads, it respects the server's own retry delay instead of guessing one, and it reports partial honestly rather than closing an order it could not fill.
The complete endpoint reference, including product creation, tickets, refunds and the bulk account job, is on the supplier API page. If you are still deciding whether a listing should be manual, inventory or auto in the first place, that choice is covered in the product management guide.



