注文を「取得する」のではなく「確保する」
GET で注文の一覧を取る作りは、走らせた数だけ同じ注文を配ります。二重納品はそこから起きます。原子的に確保する側の経路と、自分で取りに行かないと効かない三十分の札、そして誰も書いていない二つの上限をまとめました。
Sarah Johnson日本語で「api 在庫 連携」を引くと、候補に出てくるのは店舗間の在庫を同期する類の道具です。ここで説明するAPIは、その形ではありません。同期ではなく、届いた注文を自分のものとして確保して、認証情報を流し込んで閉じるための経路です。
そして最初の判断が、多くの連携が間違えるところです。どの経路で仕事を拾うか。
GET /orders は読み取りです。条件に合う注文を、聞いてきた相手全員に返し、何も変えません。作業者を二つ走らせても、一つに再試行を付けても、同じ注文が両方に渡ります。そして両方が納品しようとします。
仕事を確保する側は POST /orders-pull です。自分の手動商品に紐づく、支払い済みで未処理の注文を選び、一件ずつ「未処理のままなら処理中にする」という比較付きの更新をかけ、実際に切り替わったものだけを返します。一ミリ秒後に別の作業者が聞いても、その注文は比較に失敗して、返り値に入りません。
回す順番
- 確保する。
POST /orders-pull。本文のlimitは任意で、既定は百件、上限は五百件です。古い順に返ります。 - 時間がかかるなら、札を立てる。
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設定の画面で作ります。見出し行が無い場合は 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 が最大五百、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についての問い合わせで最も多いのがここです。三つの処理が走り、どれも応答には現れません。
- 要素は改行で分割されます。配列の一要素にファイル一つ分を入れても構いません。三行を一要素にした場合と、三要素に分けた場合は、同じ送信として扱われます。空行と前後の空白は落ちます。縦棒では分割しません。縦棒は認証情報の側の区切りとして扱われます。
- 重複は大文字小文字を無視して落とされます。その送信の中でも、その注文にすでに納品した分に対しても落とされます。通信が途切れたあとに同じ束を送り直しても、二重には入りません。
- 注文の数量を超えた行は、黙って捨てられます。残り枠までで切られ、エラーにはなりません。だから正しい件数を送ってください。
行の中身は完全に自由です。プラットフォーム側は一つの文字列として保存するだけで、解釈しません。コロン区切りの二つ組でも、二段階の鍵まで入った縦棒区切りの三つ組でも、トークンでも、セッションの塊でも、全部ただの文字列です。だから商品説明に書いた形式が、買い手が期待する形式です。それを代わりに守る仕組みはありません。
過去の納品に対する重複が拒否ではなく除去である以上、複数回に分けた納品は正式に成立する使い方です。手元にある分を送って partial とし、残りを後から送って completed にします。
上限が二つあり、どちらも今まで書かれていませんでした
orders-update には鍵ごとの最小間隔があり、既定は二百ミリ秒です。加えて利用者ごとに同時二本という上限があります。どちらを越えても 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 を読んで、その値だけ待ってください。固定の待ち時間にすると、二百ミリ秒の側には遅すぎ、同時本数の側には速すぎます。もう一つ、注文ごとの排他もあります。同じ注文に二つの納品が重なると ORDER_DELIVERY_BUSY が同じく 429 で返り、正しい対処は短く待ってから同じ内容を送り直すことです。違う認証情報を送り直すことではありません。
分けて扱う価値のある符号
API_KEY_REQUIREDとINVALID_API_KEY。見出し行が無いのか、見出し行の中身が認識されないのか。ORDER_NOT_FOUND。注文番号が見つかりません。ただし作られた直後の自動の注文は、通常の出品者にはしばらく見えないことがあるので、作成の数秒後の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 や空文字列を送った場合はすでに入っている値がそのまま残るので、更新を繰り返しても消えません。
「手動」は遅いという意味ではありません
商品タイプの手動は、注文が入ってから出品者が中身を上げる、という意味です。納品の速さの設定ではありません。そして記録がそれをはっきり示しています。
このサイトで完了した手動商品の注文六千六百四十九件のうち、支払いから完了までの時間を測れる六千四百九十九件を並べると、中央値は三十秒を切ります。四分の三強が十分以内に終わり、一日を超えるのは四十件に一件ほどです。
在庫式は行がすでに置いてあるので事実上一瞬です。自動連携は中央値では手動とほとんど変わらず、違うのは裾のほうで、こちらは長く伸びません。手動が在庫式に近い速さに見えるのは、手動で量をこなしている出品者が、画面を見張らずにこのAPIを短い間隔で回しているからです。あなたの連携が分単位の遅れを足しているなら、それは平均ではなく遅い側の裾です。
アカウントではなくSNS向けのサービスを売っている場合、時計が一つ違います。SNS向けの注文には、出品に何が書いてあっても、納品された時点から一律の三日の窓が付きます。アカウントの出品は逆で、出品ごとに設定した保証がそのまま使われ、起点は支払いではなく納品です。処理中のまま一日置かれた注文でも、窓は一秒も減っていません。
行儀のいい作業者
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) {
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,
});
}
}
この形が古い形と違うのは三点です。読むのではなく確保していること、待ち時間を推測せずサーバーが返した値に従っていること、そして埋められなかった注文を completed にせず partial と正直に報告していることです。
商品の作成や問い合わせや返金まで含めた全体の一覧はAPIのページにあります。そもそも出品を手動と在庫式と自動連携のどれにすべきかという手前の判断は、商品の設定の記事で数字を出して比べています。



