MSI 或 EXE 應用程式的 Microsoft Store 提交 API

使用適用於 MSI 或 EXE 應用程式的 Microsoft Store 提交 API 以程式設計方式查詢並為你或你組織的合作夥伴中心帳戶建立 MSI 或 EXE 應用程式提交。 如果您的帳戶管理許多應用程式,而且您想要將這些資產的提交程序自動化並最佳化,則此 API 很有用。 此 API 會使用 Microsoft Entra ID 來驗證來自應用程式或服務的呼叫。

下列步驟說明使用 Microsoft Store 提交 API 的端對端程序:

  1. 請確定您已完成所有必要條件。
  2. 在呼叫 Microsoft Store 提交 API 中的方法之前,請先取得 Microsoft Entra ID 存取權杖。 取得權杖之後,您有 60 分鐘的時間使用此權杖來呼叫 Microsoft Store 提交 API,之後權杖才會到期。 權杖過期後,您可以生成新的權杖。
  3. 呼叫 MSI 或 EXE 應用程式的 Microsoft Store 提交 API。

步驟 1:完成使用 Microsoft Store 提交 API 的必要條件

在開始編寫程式碼以呼叫 MSI 或 EXE 應用程式的 Microsoft Store 提交 API 之前,請確保已滿足以下先決條件。

  • 您 (或您的組織) 必須具有 Microsoft Entra ID 目錄,而且您必須具有目錄的全域系統管理員許可權。 如果您已經使用 Microsoft 365 或 Microsoft 的其他商務服務,則您已經有 Microsoft Entra ID 目錄。 否則,您可以在 合作夥伴中心建立新的 Microsoft Entra ID ,而不需額外付費。
  • 您必須將 Microsoft Entra ID 應用程式與您的合作夥伴中心帳戶產生關聯,並取得您的租用戶識別碼、用戶端識別碼和金鑰。 您需要這些值才能取得 Microsoft Entra ID 存取權杖,您將在呼叫 Microsoft Store 提交 API 時使用該權杖。
  • 準備您的應用程式以搭配 Microsoft Store 提交 API 使用:

如何將 Microsoft Entra ID 應用程式與您的合作夥伴中心帳戶產生關聯

您必須先將 Microsoft Entra ID 應用程式與合作夥伴中心帳戶產生關聯,擷取應用程式的租用戶識別碼和用戶端識別碼,然後產生金鑰,才能使用適用於 MSI 或 EXE 應用程式的 Microsoft 市集提交 API。 Microsoft Entra ID 應用程式代表您要從中呼叫 Microsoft Store 提交 API 的應用程式或服務。 您需要使用租戶識別碼、客戶端識別碼和金鑰來取得 Microsoft Entra ID 的存取權杖,並將其傳遞至 API。

注意

您只需要執行此工作一次。 取得租用戶識別碼、用戶端識別碼和金鑰之後,您可以在需要建立新的 Microsoft Entra ID 存取權杖時隨時重複使用它們。

  1. 在合作夥伴中心中, 將組織的合作夥伴中心帳戶與組織的 Microsoft Entra ID 目錄產生關聯
  2. 接下來,從合作夥伴中心 [帳戶設定] 區段的 [使用者] 頁面, 新增 Microsoft Entra ID 應用程式 ,代表您將用來存取合作夥伴中心帳戶提交的應用程式或服務。 請確定為此應用程式指派管理員角色。 如果您的 Microsoft Entra ID 目錄中尚未存在應用程式,您可以在 合作夥伴中心建立新的 Microsoft Entra ID 應用程式
  3. 返回 [使用者] 頁面,按一下 Microsoft Entra ID 應用程式的名稱以移至應用程式設定,然後複製 [租用戶識別碼] 和 [用戶端識別碼] 值。
  4. 若要新增金鑰或客戶端密碼,請參閱下列指示,或參閱透過 Azure 入口網站註冊應用程式的指示:

若要註冊您的應用程式:

  1. 登入 Azure 入口網站

  2. 如果您有多個租用戶的存取權,請使用頂端功能表中的「目錄 + 訂閱」篩選條件來切換要在其中註冊應用程式的租用戶。

  3. 搜尋並選取 Microsoft Entra ID 目錄。

  4. 在管理底下,選取應用程式註冊>選取您的應用程式。

  5. 選擇證書&密碼>用戶端密碼>新用戶端密碼。

  6. 新增用戶端密碼的描述。

  7. 選取祕密的到期日,或指定自訂存留期。

  8. 用戶端祕密存留期限制為兩年 (24 個月) 或更少。 您無法指定超過 24 個月的自訂存留期。

    注意

    Microsoft 建議您將到期值設定為少於 12 個月。

  9. 選取 [新增]。

  10. 記錄祕密的值,以在用戶端應用程式程式碼中使用。 離開此頁面後,就「不會再次顯示」此祕密值。

步驟 2:取得 Microsoft Entra ID 存取權杖

在呼叫適用於 MSI 或 EXE 應用程式的 Microsoft Store 提交 API 中的任何方法之前,您必須先取得 Microsoft Entra ID 存取權杖,以傳遞至 API 中每個方法的授權標頭。 取得存取權杖之後,您在其到期之前有 60 分鐘的時間可以使用。 權杖到期之後,您可以重新整理權杖,以便繼續用於對 API 的進一步呼叫。

若要取得存取權杖,請遵循使用 用戶端認證的服務對服務呼叫 中的指示,將 HTTP POST 傳送至 https://login.microsoftonline.com/<tenant_id>/oauth2/token 端點。 以下是範例要求。

POST https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token HTTP/1.1
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded; charset=utf-8

grant_type=client_credentials
&client_id=<your_client_id>
&client_secret=<your_client_secret>
&scope=https://api.store.microsoft.com/.default

針對tenant_id POST URI 和 client_idclient_secret 參數中的值,指定您在上一節中從合作夥伴中心擷取之應用程式的租使用者識別碼、用戶端識別碼和密鑰。 對於範圍參數,您必須指定https://api.store.microsoft.com/.default

您的存取權杖過期後,您可以按照此處的指示重新整理。

如需示範如何使用 C# 或 Node.js 取得存取令牌的範例,請參閱適用於 MSI 或 EXE 應用程式的 Microsoft Store 提交 API 程式代碼範例

步驟 3:使用 Microsoft Store 提交 API

擁有 Microsoft Entra ID 存取權杖之後,您可以呼叫適用於 MSI 或 EXE 應用程式的 Microsoft Store 提交 API 中的方法。 該 API 包含許多方法,這些方法被分組為應用程式的場景。 若要建立或更新提交,您通常會按特定順序呼叫多個方法。 有關每個場景和每個方法的語法的資訊,請參閱以下部分:

注意

取得存取權杖後,您有 60 分鐘的時間呼叫 MSI 或 EXE 應用程式的 Microsoft Store 提交 API 中的方法,之後權杖便會過期。

基底 URL

EXE 或 MSI 應用程式的 Microsoft Store 提交 API 的基本 URL 是:https://api.store.microsoft.com

API 合約

取得目前草稿提交中繼資料 API

取得目前草稿提交下每個模組中的中繼資料 (清單、屬性或可用性)。

路徑 [所有模組]:/submission/v1/product/{productId}/metadata?languages={languages}&includelanguagelist={true/false}
路徑 [單一模組]:/submission/v1/product/{productId}/metadata/{moduleName}?languages={languages}&includelanguagelist={true/false}
方法:GET

路徑參數

參數 描述
產品ID 產品的合作夥伴中心識別碼
模組名稱 合作夥伴中心模組 – 清單、屬性或可用性

查詢參數

參數 描述
語言 選擇性清單語言會篩選為逗號分隔字串 [限制最多 200 種語言]。

如果不存在,則檢索前 200 種可用的清單語言中繼資料。 [例如「en-us、en-gb」].
包含語言列表 選擇性 布林值 - 如果為 true,則傳回新增的清單語言的清單及其完整性狀態。

必要標頭

頁首
Authorization: Bearer <Token> 向合作夥伴中心帳戶註冊的 Microsoft Entra ID 應用程式識別碼
X-Seller-Account-Id 合作夥伴中心帳戶的賣家識別碼

回應標頭

頁首
X-Correlation-ID 每個請求的 GUID 類型唯一識別碼。 可以與支援團隊分享此資訊以分析任何問題。
Retry-After 由於速率限制,客戶端在再次呼叫 API 之前需要等待的時間 (以秒為單位)。

回應參數

名稱 類型 描述
無障礙支持 布林值
額外授權條款 繩子
可用性 物體 可用性模組資料
分類 繩子 請參閱下列類別清單
認證備註 繩子
字碼 繩子 訊息的錯誤代碼
聯絡資訊 繩子
版權 繩子
取決於驅動程式或NT 布林值
說明 繩子
由...開發 繩子
可搜尋性 繩子 [DISCOVERABLE, DEEPLINK_ONLY]
enableInFutureMarkets 布林值
錯誤 物件陣列 錯誤或警告訊息清單 (如有)
freeTrial 繩子 [NO_FREE_TRIAL,FREE_TRIAL]
硬體項目類型 繩子
隱私政策是否必需 布林值
是否推薦 布林值
必需的 布林值
是否成功 布林值
系統功能是否必需 物件陣列
語言 繩子 請參閱下列語言清單
清單 物件陣列 列出每個語言的模組資料
市場 字串陣列 請參閱下面的市場清單
訊息 繩子 錯誤的描述
最低硬體要求 繩子
最低要求 繩子
觸控筆和墨水支持 布林值
價格 繩子 [免費, 免費增值, 訂閱, 付費]
隱私政策網址 繩子
產品聲明 物體
產品功能 字串陣列
內容 物體 屬性模組資料
建議硬體配置 繩子
建議的需求 繩子
回應資料 物體 包含請求的實際回應負載
要求 物件陣列
搜尋詞 字串陣列
簡短描述 繩子
子類別 繩子 請參閱下面的子類別清單
支援聯繫資訊 繩子
系統需求詳情 物件陣列
目標 繩子 錯誤來源的實體
網站 繩子
最新動態 繩子

更新目前草稿提交中繼資料 API

在草稿提交下更新每個模組中的中繼資料。 API 檢查

  • 針對主動提交。 如果存在,則失敗並出現錯誤訊息。
  • 如果所有模組都處於就緒狀態,則可以進行儲存草稿操作。
  • 提交中的每個欄位均根據商店的要求進行驗證
  • 系統需求詳細資料驗證規則:
    • hardwareItemType 中允許的值 = 記憶體:300MB、750MB、1GB、2GB、4GB、6GB、8GB、12GB、16GB、20GB
    • hardwareItemType = DirectX 中的允許值:DX9、DX10、DX11、DX12-FEATURELEVEL11、DX12-FEATURELEVEL12
    • hardwareItemType 中允許的值 = Video_Memory:1GB、2GB、4GB、6GB

路徑 [完整模組更新]:/submission/v1/product/{productId}/metadata
方法:PUT

路徑 [模組修補檔更新]:/submission/v1/product/{productId}/metadata
方法:PATCH

API 行為

在完整模組更新 API 的情況下,整個模組資料需要出現在請求中才能完整更新每個欄位。 請求中不存在的任何欄位,其預設值用於覆蓋該特定模組的當前值。
在修補程式模組更新 API 的情況下, 只有需要更新的欄位才會出現在要求中。 請求中的這些欄位值將覆蓋其現有值,保留請求中不存在的所有其他欄位,與該特定模組的當前值相同。

路徑參數

參數 描述
產品ID 產品的合作夥伴中心識別碼

必要標頭

頁首
Authorization: Bearer <Token> 向合作夥伴中心帳戶註冊的 Microsoft Entra ID 應用程式識別碼
X-Seller-Account-Id 合作夥伴中心帳戶的賣家識別碼

要求參數

名稱 類型 描述
可用性 物體 要保存可用性模組中繼資料的物件
市場 字串陣列 必要請參閱下面的市場清單
可搜尋性 繩子 必要 [DISCOVERABLE, DEEPLINK_ONLY]
enableInFutureMarkets 布林值 必要
價格 繩子 必要 [FREE, FREEMIUM, SUBSCRIPTION, PAID]
freeTrial 繩子 如果定價是付費或訂用帳戶 [NO_FREE_TRIAL, FREE_TRIAL]
內容 物體 要保存屬性模組中繼資料的物件
隱私政策是否必需 布林值 必要
隱私政策網址 繩子 如果 isPrivacyPolicyRequired = true ,則必須是有效的 URL
網站 繩子 必須為有效的 URL
支援聯繫資訊 繩子 必須是有效的 URL 或電子郵件地址
認證備註 繩子 建議字元限制 = 2000
分類 繩子 必要請參閱下列類別清單
子類別 繩子 必要請參閱下列子類別清單
產品聲明 物體 必要
系統功能是否必需 物件陣列 [觸控,鍵盤,滑鼠,相機,NFC_HCE,NFC_Proximity,Bluetooth_LE,電話,麥克風]
必需的 布林值 必要
是否推薦 布林值 必要
硬體項目類型 繩子 必要
系統需求詳情 物件陣列 [處理器,圖形,記憶體,DirectX,顯示記憶體]
最低要求 繩子 必要 For systemRequirementsText, MaxLength = 200

hardwareItemType 中允許的值 = 記憶體:[300MB、750MB、1GB、2GB、4GB、6GB、8GB、12GB、16GB、20GB]

hardwareItemType = DirectX 中的允許值:[DX9、DX10、DX11、DX12-FEATURELEVEL11、DX12-FEATURELEVEL12]

hardwareItemType 中允許的值 = Video_Memory:[1GB、2GB、4GB、6GB]
建議的需求 繩子 必要 For systemRequirementsText, MaxLength = 200

hardwareItemType 中允許的值 = 記憶體:[300MB、750MB、1GB、2GB、4GB、6GB、8GB、12GB、16GB、20GB]

hardwareItemType = DirectX 中的允許值:[DX9、DX10、DX11、DX12-FEATURELEVEL11、DX12-FEATURELEVEL12]

hardwareItemType 中允許的值 = Video_Memory:[1GB、2GB、4GB、6GB]
取決於驅動程式或NT 布林值 必要
無障礙支持 布林值 必要
觸控筆和墨水支持 布林值 必要
清單 物體 反對列出單一語言的模組資料
語言 繩子 必要請參閱下列語言清單
說明 繩子 必要字元限制 = 10000
最新動態 繩子 字元限制 = 1500
產品功能 字串陣列 每個功能 200 個字元;最多 20 個功能
簡短描述 繩子 字元限制 = 1000
搜尋詞 字串陣列 每個搜尋字詞 30 個字元;最多 7 個搜尋字詞

所有搜尋字詞中共有 21 個唯一單字
額外授權條款 繩子 必要字元限制 = 10000
版權 繩子 字元限制 = 200
由...開發 繩子 字元限制 = 255
要求 物件陣列 每項 200 個字元; 最少和推薦之間總計最多 11 項]
最低硬體要求 繩子 字元限制 = 200
建議硬體配置 繩子 字元限制 = 200
聯絡資訊 繩子 字元限制 = 200
要添加的列表 字串陣列 請參閱下列語言清單
要移除的列表 字串陣列 請參閱下列語言清單

市場

查看表格 |市場 |縮寫 | |--------|--------------| |阿富汗 |AF | |阿爾巴尼亞 |AL | |阿爾及利亞 |DZ | |美屬薩摩亞 |AS | |安道爾 |AD | |安哥拉 |AO | |安圭拉 |AI | |南極洲 |AQ | |安提瓜和巴布達 |AG | |阿根廷 |AR | |亞美尼亞 |AM | |阿魯巴 |AW | |澳大利亞 |AU | |奧地利 |AT | |亞塞拜然 |AZ | |巴哈馬 |BS | |巴林 |BH | |孟加拉 |BD | |巴貝多 |BB | |白俄羅斯 |BY | |比利時 |BE | |伯利茲 |BZ | |貝南 |BJ | |百慕達 |BM | |不丹 |BT | |委內瑞拉玻利瓦爾共和國 |VE | |玻利維亞 |BO | |博內爾 |BQ | |波斯尼亞和黑塞哥維那 |BA | |博茨瓦納 |BW | |布維島 |BV | |巴西 |BR | |英屬印度洋領地 |IO | |英屬維爾京群島 |VG | |汶萊 |BN | |保加利亞 |BG | |布吉納法索 |BF | |蒲隆地 |BI | |柬埔寨 |KH | |喀麥隆 |CM | |加拿大 |CA | |佛得角 |CV | |開曼群島 |KY | |中非共和國 |CF | |乍得 |TD | |智利 |CL | |中國 |CN | |聖誕島 |CX | |科科斯(基林)群島 |CC | |哥倫比亞 |CO | |科摩羅 |KM | |剛果 |CG | |剛果(金)|CD | |庫克群島 |CK | |哥斯大黎加 |CR | |克羅埃西亞 |HR | |庫拉索 |CW | |塞浦路斯 |CY | |捷克共和國 |CZ | |科特迪瓦 |CI | |丹麥 |DK | |吉布地 |DJ | |多明尼加 |DM | |多明尼加共和國 |DO | |厄瓜多爾 |EC | |埃及 |EG | |薩爾瓦多 |SV | |赤道幾內亞 |GQ | |厄立特里亞 |ER | |愛沙尼亞 |EE | |埃塞俄比亞 |ET | |福克蘭群島 |FK | |法羅群島 |FO | |斐濟 |FJ | |芬蘭 |FI | |法國 |FR | |法屬圭亞那 |GF | |法屬波利尼西亞 |PF | |法屬南部和南極地區 |TF | |加蓬 |GA | |岡比亞 |GM | |喬治亞 |GE | |德國 |DE | |加納 |GH | |直布羅陀 |GI | |希臘 |GR | |格陵蘭 |GL | |格林納達 |GD | |瓜德羅普 |GP | |關島 |GU | |瓜地馬拉 |GT | |根西 |GG | |幾內亞 |GN | |幾內亞比紹 |GW | |圭亞那 |GY | |海地 |HT | |赫德島和麥克唐納群島 |HM | |梵蒂岡 |VA | |宏都拉斯 |HN | |香港特別行政區 |HK | |匈牙利 |HU | |冰島 |IS | |印度 |IN | |印尼 |ID | |伊拉克 |IQ | |愛爾蘭 |IE | |以色列 |IL | |義大利 |IT | |牙買加 |JM | |日本 |JP | |澤西 |JE | |約旦 |JO | |哈薩克斯坦 |KZ | |肯亞 |KE | |吉里巴斯 |KI | |韓國 |KR | |科威特 |KW | |吉爾吉斯斯坦 |KG | |老撾 |LA | |拉脫維亞 |LV | |黎巴嫩 |LB | |萊索托 |LS | |賴比瑞亞 |LR | |利比亞 |LY | |列支敦士登 |LI | |立陶宛 |LT | |盧森堡 |LU | |澳門特別行政區 |MO | |北馬其頓 |MK | |馬達加斯加 |MG | |馬拉威 |MW | |馬來西亞 |MY | |馬爾地夫 |MV | |馬利 |ML | |馬耳他 |MT | |馬恩島 |IM | |馬紹爾群島 |MH | |馬提尼克 |MQ | |毛里塔尼亞 |MR | |毛里求斯 |MU | |馬約特 |YT | |墨西哥 |MX | |密克羅尼西亞 |FM | |摩爾多瓦 |MD | |摩納哥 |MC | |蒙古 |MN | |黑山 - ME | |蒙特塞拉特 |MS | |摩洛哥 |MA | |莫桑比克 |MZ | |緬甸 |MM | |納米比亞 |NA | |瑙魯 |NR | |尼泊爾 |NP | |荷蘭 |NL | |新喀裡多尼亞 |NC | |新西蘭 |NZ | |尼加拉瓜 |NI | |尼日爾 |NE | |尼日利亞 |NG | |紐埃 |NU | |諾福克島 |NF | |北馬里亞納群島 |MP | |挪威 |NO | |阿曼 |OM | |巴基斯坦 |PK | |帛琉 |PW | |巴勒斯坦權力機構 |PS | |巴拿馬 |PA | |巴布亞新幾內亞 |PG | |巴拉圭 |PY | |秘魯 |PE | |菲律賓 |PH | |皮特凱恩島 |PN | |波蘭 |PL | |葡萄牙 |PT | |卡塔爾 |QA | |留尼汪 |RE | |羅馬尼亞 |RO | |俄羅斯 |RU | |盧安達 |RW | |聖巴泰勒米 |BL | |聖赫勒拿、阿森松和特里斯坦-達庫尼亞 |SH | |聖基茨和尼維斯 |KN | |聖露西亞 |LC | |聖馬丁(法語部分) |MF | |聖皮埃爾以及密克隆群島 |PM | |聖文森特和格林納丁斯 |VC | |薩摩亞 |WS | |聖馬力諾 |SM | |沙特阿拉伯 |SA | |塞內加爾 |SN | |塞爾維亞 |RS | |塞舌爾 |SC | |塞拉利昂 |SL | |新加坡 |SG | |聖馬丁(荷蘭部分) |SX | |斯洛伐克 |SK | |斯洛文尼亞 |SI | |所羅門群島 |SB | |索馬里 |SO | |南非 |ZA | |南喬治亞島和南桑威奇群島 |GS | |西班牙 |ES | |斯里蘭卡 |LK | |蘇利南 |SR | |斯瓦爾巴和揚馬延 |SJ | |史瓦濟蘭 |SZ | |瑞典 |SE | |瑞士 |CH | |聖多美和普林西比 |ST | |台灣 |TW | |塔吉克斯坦 |TJ | |坦尚尼亞 |TZ | |泰國 |TH | |東帝汶 |TL | |多哥 |TG | |托克勞 |TK | |湯加 |TO | |千里達托貝哥 |TT | |突尼西亞 |TN | |土耳其 |TR | |土庫曼斯坦 |TM | |特克斯和凱科斯群島 |TC | |圖瓦盧 |TV | |美國小離島 |UM | |美屬維京群島 |VI | |烏干達 |UG | |烏克蘭 |UA | |阿拉伯聯合大公國 |AE | |英國 |GB | |美國 |US | |烏拉圭 |UY | |烏茲別克斯坦 |UZ | |瓦努阿圖 |VU | |越南 |VN | |瓦利斯和富圖納 |WF | |也門 |YE | |贊比亞 |ZM | |津巴布韋 |ZW | |奧蘭群島 |AX |

類別和子類別

查看表格 |類別 |子類別 | |-----------------------|---------------| |書籍和參考 |電子閱讀器,小說,非小說,參考 | |商業 |會計與財務、協作、CRM、數據與分析、文件管理、庫存與物流、法律與人力資源、項目管理、遠程桌面、銷售與營銷、時間與費用 | |開發人員工具 |資料庫、設計工具、開發套件、網路、參考和培訓、伺服器、工具程式、網頁寄宿 |教育 |教育書籍和參考、早期學習、教學工具、語言、學習輔助工具 | |娛樂 |(無) | |美食與餐飲 |(無) | |政府與政治 |(無) | |健康與健身 |(無) | |兒童與家庭 |兒童和家庭書籍和參考資料、兒童和家庭娛樂、愛好和玩具、體育和活動、兒童和家庭旅行 | |生活風格 |汽車、DIY、家庭與花園、人際關係、特殊興趣、風格與時尚 | |醫療 |(無) | |多媒體設計 |插畫與平面設計、音樂製作、攝影與錄像製作 | |音樂 |(無) | |導航和地圖 |(無) | |新聞與天氣 |新聞,天氣 | |個人理財 |銀行與投資、預算與稅收 | |個人化 |鈴聲和聲音、主題、壁紙和鎖屏 | |照片和視頻 |(無) | |生產力 |(無) | |安全 |電腦防護、個人安全 | |購物 |(無) | |社會 |(無) | |運動 |(無) | |旅遊 |城市指南,酒店 | |實用程式和工具 |備份和管理、檔案管理器 |

語言

查看表格 |語言名稱 |支援的語言代碼 | |---------------|--------------------------| |南非荷蘭語 |af, af-za | |阿爾巴尼亞語 |sq, sq-al | |阿姆哈拉語 |am, am-et | |亞美尼亞語 |hy, hy-am | |阿薩姆語 |as, as-in | |亞塞拜然語 |az-arab, az-arab-az, az-cyrl, az-cyrl-az, az-latn, az-latn-az | |巴斯克語(巴斯克語) |eu, eu-es | |白俄羅斯語 |be, be-by | |孟加拉語 |bn, bn-bd, bn-in | |波斯尼亞語 |bs, bs-cyrl, bs-cyrl-ba, bs-latn, bs-latn-ba | |保加利亞語 |bg, bg-bg | |加泰羅尼亞語 |ca, ca-es, ca-es-valencia | |切羅基人 |chr-cher, chr-cher-us, chr-latn | |簡體中文 |zh-Hans, zh-cn, zh-hans-cn, zh-sg, zh-hans-sg | |繁體中文 |zh-Hant, zh-hk, zh-mo, zh-tw, zh-hant-hk, zh-hant-mo, zh-hant-tw | |克羅埃西亞語 |hr, hr-hr, hr-ba | |捷克語 |cs, cs-cz | |丹麥語 |da, da-dk | |達里 |prs, prs-af, prs-arab | |荷蘭語 |nl, nl-nl, nl-be | |英語 |en, en-au, en-ca, en-gb, en-ie, en-in, en-nz, en-sg, en-us, en-za, en-bz, en-hk, en-id, en-jm, en-kz, en-mt, en-my, en-ph, en-pk, en-tt, en-vn, en-zw | |愛沙尼亞語 |et, et-ee | |菲律賓 |fil, fil-latn, fil-ph | |芬蘭語 |fi, fi-fi | |法語 |fr, fr-be , fr-ca , fr-ch , fr-fr , fr-lu, fr-cd, fr-ci, fr-cm, fr-ht, fr-ma, fr-mc, fr-ml, fr-re, frc-latn, frp-latn | |加利西亞語 |gl, gl-es | |格魯吉亞語 |ka, ka-ge | |德語 |de, de-at, de-ch, de-de, de-lu, de-li | |希臘語 |el, el-gr | |古吉拉特語 |gu, gu-in | |豪薩語 |ha, ha-latn, ha-latn-ng | |希伯來語 |he, he-il | |印地語 |hi, hi-in | |匈牙利語 |hu, hu-hu | |冰島語 |is, is-is | |伊博語 |ig, ig-latn, ig-ng | |印尼語 |id, id-id | |因紐特語(拉丁語) |iu-cans, iu-latn, iu-latn-ca | |愛爾蘭語 |ga, ga-ie | |科薩語 |xh, xh-za | |祖魯語 |zu, zu-za | |義大利語 |it, it-it, it-ch | |日語 |ja, ja-jp | |卡納達語 |kn, kn-in | |哈薩克語 |kk, kk-kz | |高棉語 |km, km-kh | |基切語 |quc-latn, qut-gt, qut-latn | |吉尼亞盧旺達語 |rw, rw-rw | |斯瓦希里語 |sw, sw-ke | |孔卡尼語 |kok, kok-in | |韓語 |ko, ko-kr | |庫爾德語 |ku-arab, ku-arab-iq | |吉爾吉斯語 |ky-kg, ky-cyrl | |寮語 |lo, lo-la | |拉脫維亞語 |lv, lv-lv | |立陶宛語 |lt, lt-lt | |盧森堡語 |lb, lb-lu | |馬其頓語 |mk, mk-mk | |馬來語 |ms, ms-bn, ms-my | |馬拉雅拉姆語 |ml, ml-in | |馬耳他語 |mt, mt-mt | |毛利語 |mi, mi-latn, mi-nz | |馬拉地語 |mr, mr-in | |蒙古文(西里爾文) |mn-cyrl, mn-mong, mn-mn, mn-phag | |尼泊爾語 |ne, ne-np | |挪威語 |nb, nb-no, nn, nn-no, no, no-no | |奧里亞語 |or, or-in | |波斯語 |fa, fa-ir | |波蘭語 |pl, pl-pl | |葡萄牙語(巴西) |pt-br | |葡萄牙語(葡萄牙) |pt, pt-pt | |旁遮普語 |pa, pa-arab, pa-arab-pk, pa-deva, pa-in | |蓋丘亞語 |quz, quz-bo, quz-ec, quz-pe | |羅馬尼亞語 |ro, ro-ro | |俄語 |ru, ru-ru | |蘇格蘭蓋爾語 |gd-gb, gd-latn | |塞爾維亞語(拉丁文) |sr-Latn, sr-latn-cs, sr, sr-latn-ba, sr-latn-me, sr-latn-rs | |塞爾維亞語(西里爾字母) |sr-cyrl, sr-cyrl-ba, sr-cyrl-cs, sr-cyrl-me, sr-cyrl-rs | |塞索托語 |nso, nso-za | |茨瓦納語 |tn, tn-bw, tn-za | |信德語 |sd-arab, sd-arab-pk, sd-deva | |僧伽羅語 |si, si-lk | |斯洛伐克語 |sk, sk-sk | |斯洛維尼亞語 |sl, sl-si | |西班牙語 |es, es-cl, es-co, es-es, es-mx, es-ar, es-bo, es-cr, es-do, es-ec, es-gt, es-hn, es-ni, es-pa, es-pe, es-pr, es-py, es-sv, es-us, es-uy, es-ve | |瑞典語 |sv, sv-se, sv-fi | |塔吉克語(西里爾字母) |tg-arab, tg-cyrl, tg-cyrl-tj, tg-latn | |泰米爾語 |ta, ta-in | |韃靼語 |tt-arab, tt-cyrl, tt-latn, tt-ru | |泰盧固語 |te, te-in | |泰語 |th, th-th | |提格利尼亞語 |ti, ti-et | |土耳其語 |tr, tr-tr | |土庫曼語 |tk-cyrl, tk-latn, tk-tm, tk-latn-tr, tk-cyrl-tr | |烏克蘭語 |uk, uk-ua | |烏爾都語 |ur, ur-pk | |維吾爾語 |ug-arab, ug-cn, ug-cyrl, ug-latn | |烏茲別克語(拉丁文) |uz, uz-cyrl, uz-latn, uz-latn-uz | |越南語 |vi, vi-vn | |威爾斯語 |cy, cy-gb | |沃洛夫語 |wo, wo-sn | |約魯巴語 |yo-latn, yo-ng |

範例要求

{
    "availability":{
        "markets": ["US"],
        "discoverability": "DISCOVERABLE",
        "enableInFutureMarkets": true,
        "pricing": "PAID",
        "freeTrial": "NO_FREE_TRIAL"
    },
    "properties":{
        "isPrivacyPolicyRequired": true,
        "privacyPolicyUrl": "http://contoso.com",
        "website": "http://contoso.com",
        "supportContactInfo": "http://contoso.com",
        "certificationNotes": "Certification Notes",
        "category": "DeveloperTools",
        "subcategory": "Database",
        "productDeclarations": {
            "dependsOnDriversOrNT": false,
            "accessibilitySupport": false,
            "penAndInkSupport": false
        },
        "isSystemFeatureRequired": [
        {
            "isRequired": true,
                "isRecommended": false,
                "hardwareItemType": "Touch"
            },
            {
                "isRequired": true,
                "isRecommended": false,
                "hardwareItemType": "Keyboard"
            },
            {
                "isRequired": false,
                "isRecommended": false,
                "hardwareItemType": "Mouse"
            },
            {
                "isRequired": false,
                "isRecommended": false,
                "hardwareItemType": "Camera"
            },
            {
                "isRequired": false,
                "isRecommended": false,
                "hardwareItemType": "NFC_HCE"
            },
            {
                "isRequired": false,
                "isRecommended": false,
                "hardwareItemType": "NFC_Proximity"
            },
            {
                "isRequired": false,
                "isRecommended": false,
                "hardwareItemType": "Bluetooth_LE"
            },
            {
                "isRequired": false,
                "isRecommended": false,
                "hardwareItemType": "Telephony"
            },
            {
                "isRequired": false,
                "isRecommended": false,
                "hardwareItemType": "Microphone"
            }
        ],
        "systemRequirementDetails": [
            {
                "minimumRequirement": "1GB",
                "recommendedRequirement": "4GB",
                "hardwareItemType": "Memory"
            },
            {
                "minimumRequirement": "",
                "recommendedRequirement": "",
                "hardwareItemType": "DirectX"
            },
            {
                "minimumRequirement": "",
                "recommendedRequirement": "",
                "hardwareItemType": "Video_Memory"
            },
            {
                "minimumRequirement": "",
                "recommendedRequirement": "",
                "hardwareItemType": "Processor"
            },
            {
                "minimumRequirement": "",
                "recommendedRequirement": "",
                "hardwareItemType": "Graphics"
            }
        ]
    },
    "listings":{
        "language": "en-us",
        "description": "Description",
        "whatsNew": "What's New",
        "productFeatures": ["Feature 1"],
        "shortDescription": "Short Description",
        "searchTerms": ["Search Ter 1"],
        "additionalLicenseTerms": "License Terms",
        "copyright": "Copyright Information",
        "developedBy": "Developer Details",
        "sortTitle": "Product 101",
        "requirements": [
            {
                "minimumHardware": "Pentium4",
                "recommendedHardware": "Corei9"
            }
        ],
        "contactInfo": "contactus@contoso.com"               
    },      
    "listingsToAdd": ["en-au"],
    "listingsToRemove": ["en-gb"]
}

回應標頭

頁首
X-Correlation-ID 每個請求的 GUID 類型唯一識別碼。 可以與支援團隊分享此資訊以分析任何問題。
Retry-After 由於速率限制,客戶端在再次呼叫 API 之前需要等待的時間 (以秒為單位)

回應參數

名稱 類型 描述
是否成功 布林值
錯誤 物件陣列 錯誤或警告訊息清單 (如有)
字碼 繩子 訊息的錯誤代碼
訊息 繩子 錯誤的描述
目標 繩子 錯誤來源的實體
回應資料 物體 包含請求的實際回應負載
輪詢網址 繩子 輪詢 URL 以取得任何進行中提交的狀態
持續提交ID 繩子 任何進行中提交的提交標識碼

範例回應

{
    "isSuccess": true,
    "errors": [{
        "code": "badrequest",
        "message": "Error Message 1",
        "target": "listings"
        }, {
        "code": "warning",
        "message": "Warning Message 1",
        "target": "properties"
    }],
    "responseData": {
        "pollingUrl": "/submission/v1/product/{productId}/submission/{submissionId}/status",
        "ongoingSubmissionId": ""
    } 
}

取得目前的草稿套件 API

擷取目前草稿提交下的套件詳細資料。

路徑 [所有套件]:/submission/v1/product/{productId}/packages
方法:GET

路徑 [單一套件]:/submission/v1/product/{productId}/packages/{packageId}
方法:GET

路徑參數

名稱 描述
產品ID 產品的合作夥伴中心識別碼
軟體包ID 要取得的封裝唯一識別碼 (ID)。

必要標頭

頁首
Authorization: Bearer <Token> 使用向合作夥伴中心帳戶註冊的 Microsoft Entra ID 應用程式識別碼
X-Seller-Account-Id 合作夥伴中心帳戶的賣家識別碼

回應標頭

頁首
X-Correlation-ID 每個請求的 GUID 類型唯一識別碼。 可以與支援團隊分享此資訊以分析任何問題。
Retry-After 由於速率限制,客戶端在再次呼叫 API 之前需要等待的時間 (以秒為單位)。

回應參數

名稱 類型 描述
是否成功 布林值
錯誤 物件陣列 錯誤或警告訊息清單 (如有)
字碼 繩子 訊息的錯誤代碼
訊息 繩子 錯誤的描述
目標 繩子 錯誤來源的實體
回應資料 物體
套件 物件陣列 要保存封裝模組資料的物件
軟體包ID 繩子
套件網址 繩子
語言 字串陣列
架構 字串陣列 [Neutral, X86, X64, Arm, Arm64]
isSilentInstall(靜默安裝) 布林值 如果您的安裝程式以無訊息模式執行,而不需要切換或其他 false,則這應該標示為 true
安裝參數 繩子
genericDocUrl 繩子
錯誤詳情 物件陣列
錯誤場景 繩子
錯誤情境詳情 物件陣列
錯誤值 繩子
錯誤網址 繩子
PackageType 繩子

範例回應

{   
    "isSuccess": true,
    "errors": [{
        "code": "badrequest",
        "message": "Error Message 1",
        "target": "listings"
    }, {
        "code": "warning",
        "message": "Warning Message 1",
        "target": "properties"
    }],
    "responseData":{
        "packages":[{
            "packageId": "pack0832",
            "packageUrl": "https://www.contoso.com/downloads/1.1/setup.exe",
            "languages": ["en-us"],
            "architectures": ["X86"],
            "isSilentInstall": true,
            "installerParameters": "/s",
            "genericDocUrl": "https://docs.contoso.com/doclink",
            "errorDetails": [{
                "errorScenario": "rebootRequired",
                "errorScenarioDetails": [{
                    "errorValue": "ERR001001",
                    "errorUrl": "https://errors.contoso.com/errors/ERR001001"
                }]
            }],
            "packageType": "exe",
        }]
    }
}

更新目前的草稿套件 API

更新目前草稿提交下的套件詳細資料。

路徑 [完整模組更新]:/submission/v1/product/{productId}/packages
方法:PUT

路徑 [單一套件修補檔更新]:/submission/v1/product/{productId}/packages/{packageId}
方法:PATCH

API 行為

在完整模組更新 API 的情況下,整個套件資料需要出現在請求中才能完整更新每個欄位。 請求中不存在的任何欄位,其預設值用於覆蓋該特定模組的當前值。 這會導致從要求使用一組新套件覆寫所有現有的套件。 這會導致套件標識碼的重新產生,而用戶應該呼叫 GET Packages API 以取得最新的套件識別碼。

在單一套件修補程式更新 API 的案例中,只有要針對指定套件更新的欄位必須存在於要求中。 請求中的這些欄位值將覆蓋其現有值,保留請求中不存在的所有其他字段,與該特定套件的當前值相同。 集合中的其他套件會維持原樣。

路徑參數

名稱 描述
產品ID 產品的合作夥伴中心識別碼
軟體包ID 封裝的唯一識別碼

必要標頭

頁首
Authorization: Bearer <Token> 使用向合作夥伴中心帳戶註冊的 Microsoft Entra ID 應用程式識別碼
X-Seller-Account-Id 合作夥伴中心帳戶的賣家識別碼

要求參數

名稱 類型 描述
套件 物件陣列 保留封裝模組資料的物件 [僅適用於完整模組更新的必要專案]
套件網址 繩子 必要
語言 字串陣列 必要
架構 字串陣列 必要 應包含單一架構 - Neutral、X86、X64、Arm、Arm64
isSilentInstall(靜默安裝) 布林值 必要 如果您的安裝程式以無訊息模式執行,而不需要切換或其他 false,則這應該標示為 true
安裝參數 繩子 如果isSilentInstall 為 false,則為必要項
genericDocUrl 繩子 如果 packageType 是 exe 包含 EXE 類型安裝程式的自訂錯誤代碼詳細資訊的文件連結
錯誤詳情 物件陣列 用於保存 EXE 類型安裝程式的自訂錯誤代碼和詳細資訊的中繼資料。
錯誤場景 繩子 識別特定的錯誤案例。 [安裝已被使用者取消,應用程式已存在,安裝已在進行中,磁碟空間已滿,需要重新開機,網路故障,安裝過程中封包被拒,安裝成功,其他]
錯誤情境詳情 物件陣列
錯誤值 繩子 安裝期間可能出現的錯誤碼
錯誤網址 繩子 取得錯誤詳細資料的 URL
PackageType 繩子 必要 [exe, msi]

範例要求 [完整模組更新]

{
    "packages":[{
        "packageUrl": "https://www.contoso.com/downloads/1.1/setup.exe",
        "languages": ["en-us"],
        "architectures": ["X86"],
        "isSilentInstall": true,
        "installerParameters": "/s",
        "genericDocUrl": "https://docs.contoso.com/doclink",
        "errorDetails": [{
            "errorScenario": "rebootRequired",
            "errorScenarioDetails": [{
                "errorValue": "ERR001001",
                "errorUrl": "https://errors.contoso.com/errors/ERR001001"
            }]
        }],
        "packageType": "exe",
    }]
}

範例要求 [單一套件修補程式更新]

{
    "packageUrl": "https://www.contoso.com/downloads/1.1/setup.exe",
    "languages": ["en-us"],
    "architectures": ["X86"],
    "isSilentInstall": true,
    "installerParameters": "/s",
    "genericDocUrl": "https://docs.contoso.com/doclink",
    "errorDetails": [{
        "errorScenario": "rebootRequired",
        "errorScenarioDetails": [{
            "errorValue": "ERR001001",
            "errorUrl": "https://errors.contoso.com/errors/ERR001001"
        }]
    }],
    "packageType": "exe",
}

回應標頭

頁首
X-Correlation-ID 每個請求的 GUID 類型唯一識別碼。 可以與支援團隊分享此資訊以分析任何問題。
Retry-After 由於速率限制,客戶端在再次呼叫 API 之前需要等待的時間 (以秒為單位)。

回應參數

名稱 類型 描述
是否成功 布林值
錯誤 物件陣列 [錯誤或警告訊息清單 (如有)]
字碼 繩子 訊息的錯誤代碼
訊息 繩子 錯誤的描述
目標 繩子 錯誤來源的實體
回應資料 物體
輪詢網址 繩子 [輪詢 URL 以取得任何已進行中的提交狀態]
持續提交ID 繩子 [任何進行中提交的提交標識碼]

範例回應

{
    "isSuccess": true,
    "errors": [{
        "code": "badrequest",
        "message": "Error Message 1",
        "target": "listings"
        }, {
        "code": "warning",
        "message": "Warning Message 1",
        "target": "properties"
    }],
    "responseData": {
        "pollingUrl": "/submission/v1/product/{productId}/submission/{submissionId}/status",
        "ongoingSubmissionId": ""
    } 
}

認可套件 API

在目前的草稿提交下,認可使用套件更新 API 更新的新套件集。 此 API 會傳迴輪詢 URL 來追蹤套件上傳。

路徑:/submission/v1/product/{productId}/packages/commit
方法:POST

路徑參數

名稱 描述
產品ID 產品的合作夥伴中心識別碼

必要標頭

頁首
Authorization: Bearer <Token> 使用在合作夥伴中心帳戶中註冊的 Microsoft Entra ID 應用程式 ID
X-Seller-Account-Id 合作夥伴中心帳戶的賣家識別碼

回應標頭

頁首
X-Correlation-ID 每個請求的 GUID 類型唯一識別碼。 可以與支援團隊分享此資訊以分析任何問題。
Retry-After 由於速率限制,客戶端在再次呼叫 API 之前需要等待的時間 (以秒為單位)。

回應參數

名稱 類型 描述
是否成功 布林值
錯誤 物件陣列 [錯誤或警告訊息清單 (如有)]
字碼 繩子 訊息的錯誤代碼
訊息 繩子 錯誤的描述
目標 繩子 錯誤來源的實體
回應資料 物體
輪詢網址 繩子 [輪詢 URL 以取得套件上傳或提交狀態,以防任何已進行中提交]
持續提交ID 繩子 [任何進行中提交的提交標識碼]

範例回應

{
    "isSuccess": true,
    "errors": [{
        "code": "badrequest",
        "message": "Error Message 1",
        "target": "listings"
        }, {
        "code": "warning",
        "message": "Warning Message 1",
        "target": "properties"
    }],
    "responseData": {
        "pollingUrl": "/submission/v1/product/{productId}/status",
        "ongoingSubmissionId": ""
    } 
}

取得目前的草稿清單資產 API

擷取目前草稿提交下的資產詳細資料。

路徑:/submission/v1/product/{productId}/listings/assets?languages={languages}
方法:GET

路徑參數

名稱 描述
產品ID 產品的合作夥伴中心識別碼

查詢參數

名稱 描述
語言 [選擇性] 清單語言會篩選為逗號分隔字串 [限制最多 200 種語言]。 如果不存在,則會擷取前 200 個可用清單語言的資產資料。 (例如「en-us、en-gb」)

必要標頭

頁首
Authorization: Bearer <Token> 使用 Microsoft Entra ID 應用程式識別碼來註冊合作夥伴中心帳戶
X-Seller-Account-Id 合作夥伴中心帳戶的賣家識別碼

回應標頭

頁首
X-Correlation-ID 每個請求的 GUID 類型唯一識別碼。 可以與支援團隊分享此資訊以分析任何問題。
Retry-After 由於速率限制,客戶端在再次呼叫 API 之前需要等待的時間 (以秒為單位)。

回應參數

名稱 類型 描述
是否成功 布林值
錯誤 物件陣列 錯誤或警告訊息清單 (如有)
字碼 繩子 訊息的錯誤代碼
訊息 繩子 錯誤的描述
目標 繩子 錯誤來源的實體
回應資料 物體
資產列表 物件陣列 列出每個語言的資產詳細資料
語言 繩子
店鋪標誌 物件陣列
螢幕擷取畫面 物件陣列
識別碼 繩子
資產網址 繩子 必須為有效的 URL
影像大小 物體
寬度 整數
高度 整數

範例回應

{   
"isSuccess": true,
    "errors": [{
        "code": "badrequest",
        "message": "Error Message 1",
        "target": "listings"
        }, {
        "code": "warning",
        "message": "Warning Message 1",
        "target": "properties"
    }],
    "responseData":{
        "listingAssets": [{
            "language": "en-us",
            "storeLogos": [
                {
                    "id": "1234567890abcdefgh",
                    "assetUrl": "https://contoso.com/blob=1234567890abcdefgh",
                    "imageSize": {
                        "width": 2160,
                        "height": 2160
                    }
                }
            ],
            "screenshots": [
                {
                    "id": "1234567891abcdefgh",
                    "assetUrl": "https://contoso.com/blob=1234567891abcdefgh",
                    "imageSize": {
                        "width": 2160,
                        "height": 2160
                    }
                }
            ]
        }]
    }
}

建立清單資產 API

在目前的草稿提交下建立新的清單資產上傳。

列出資產的更新

適用於 EXE 或 MSI 應用程式的 Microsoft Store 提交 API 會針對每個個別映像資產上傳使用執行時間產生的 SAS URL,以及上傳成功之後的認可 API 呼叫。 為了能夠更新清單資源,並能夠在清單模組中新增/刪除區域設置,可以使用以下方法:

  1. 使用建立清單資產 API 來傳送有關資產上傳的要求,以及資產的語言、類型和計數。
  2. 根據請求的資產數量,按需建立資產 ID,並建立短期 SAS URL 並將其發送回資產類型下的回應正文中。 您可以使用此 URL 透過 HTTP 用戶端上傳特定類型的映像資產 [Put Blob (REST API) - Azure 儲存體 | Microsoft Docs]。
  3. 上傳後,您還可以使用提交清單資產 API 發送先前從先前的 API 呼叫中收到的新資產識別碼資訊。 在驗證之後,單一 API 會在內部認可列出資產資料。
  4. 此方法將有效地覆寫在要求中傳送的特定語言下資產類型上一組先前的影像。 因此,將會移除先前上傳的資產。

路徑:/submission/v1/product/{productId}/listings/assets/create
方法:POST

路徑參數

名稱 描述
產品ID 產品的合作夥伴中心識別碼

必要標頭

頁首 描述
Authorization: Bearer <Token> 使用向合作夥伴中心帳戶註冊的 Microsoft Entra ID 應用程式識別碼
X-Seller-Account-Id 合作夥伴中心帳戶的賣家識別碼

要求參數

名稱 類型 描述
語言 繩子 必要
建立資產請求 物體 必要
螢幕截圖 整數 如果ISV需要更新螢幕快照或新增清單語言 [1 - 10],則為必要專案
標誌 整數 如果ISV需要更新螢幕快照或新增清單語言 [1 或 2],則為必要專案

回應標頭

頁首 描述
X-Correlation-ID 每個請求的 GUID 類型唯一識別碼。 可以與支援團隊分享此資訊以分析任何問題。
Retry-After 由於速率限制,客戶端在再次呼叫 API 之前需要等待的時間 (以秒為單位)。

回應參數

名稱 類型 描述
是否成功 布林值
錯誤 物件陣列 錯誤或警告訊息清單 (如有)
字碼 繩子 訊息的錯誤代碼
訊息 繩子 錯誤的描述
目標 繩子 錯誤來源的實體
回應資料 物體
資產列表 物體 包含要上傳的 StoreLogos 和螢幕擷取畫面詳細資訊的物件
語言 繩子
店鋪標誌 物件陣列
螢幕擷取畫面 物件陣列
識別碼 繩子
主要資產上傳網址 繩子 使用 Azure Blob REST API 上傳清單資產的主要 URL
secondaryAssetUploadUrl(次要資產上傳URL) 繩子 使用 Azure Blob REST API 上傳清單資產的次要 URL
httpMethod HTTP 方法 HTTP 方法必須用來透過資產上傳 URL 上傳資產 – 主要或次要
HTTP 標頭 物體 一個物件,其鍵作為所需標頭出現在對資產上傳 URL 的上傳 API 呼叫中。 如果值不是空的,標頭必須有特定值。 否則,在 API 呼叫期間計算值。

範例回應

{
    "isSuccess": true,
    "errors": [{
        "code": "badrequest",
        "message": "Error Message 1",
        "target": "listings"
        }, {
        "code": "warning",
        "message": "Warning Message 1",
        "target": "properties"
    }],
    "responseData": {
        "listingAssets": {
            "language": "en-us",
            "storeLogos":[{
                "id": "1234567890abcdefgh",
                "primaryAssetUploadUrl": "https://contoso.com/upload?blob=1234567890abcdefgh&sig=12345",
                "secondaryAssetUploadUrl": "https://contoso.com/upload?blob=0987654321abcdfger&sig=54326",
                "httpMethod": "PUT",
                "httpHeaders": {"Required Header Name": "Header Value"}
            }],
            "screenshots":[{
                "id": "0987654321abcdfger",
                "primaryAssetUploadUrl": "https://contoso.com/upload?blob=0987654321abcdfger&sig=54321",
                "secondaryAssetUploadUrl": "https://contoso.com/upload?blob=0987654321abcdfger&sig=54322",
                "httpMethod": "PUT",
                "httpHeaders": {"Required Header Name": "Header Value"}

            }]
        }
    } 
}

認可清單資產 API

認可使用目前草稿提交下建立資產 API 的詳細資料上傳的新清單資產。

路徑:/submission/v1/product/{productId}/listings/assets/commit
方法:PUT

路徑參數

名稱 描述
產品ID 產品的合作夥伴中心識別碼

必要標頭

頁首 描述
Authorization: Bearer <Token> 使用向合作夥伴中心帳戶註冊的 Microsoft Entra ID 應用程式識別碼
X-Seller-Account-Id 合作夥伴中心帳戶的賣家識別碼

要求參數

名稱 類型 描述
資產列表 物體
語言 繩子
店鋪標誌 物件陣列
螢幕擷取畫面 物件陣列
識別碼 繩子 應該是使用者想要從取得目前列表資產 API 保存的現有識別碼,或是建立清單資產 API 中上傳新資產的新標識碼。
資產網址 繩子 應該是使用者希望從取得目前清單資產 API 中保留的現有資產的 URL,或是上傳 URL (主要或次要),使用該 URL 在建立清單資產 API 中上傳新資產。 必須為有效的 URL

範例要求

{
    "listingAssets": { 
        "language": "en-us",    
        "storeLogos": [
            {
                "id": "1234567890abcdefgh",
                "assetUrl": "https://contoso.com/blob=1234567890abcdefgh",
            }
        ],
        "screenshots": [
            {
                "id": "1234567891abcdefgh",
                "assetUrl": "https://contoso.com/blob=1234567891abcdefgh",
            }
        ]
    }
}

回應標頭

頁首 描述
X-Correlation-ID 每個請求的 GUID 類型唯一識別碼。 可以與支援團隊分享此資訊以分析任何問題。
Retry-After 由於速率限制,客戶端在再次呼叫 API 之前需要等待的時間 (以秒為單位)。

回應參數

名稱 類型 描述
是否成功 布林值
錯誤 物件陣列 錯誤或警告訊息清單 (如有)
字碼 繩子 訊息的錯誤代碼
訊息 繩子 錯誤的描述
目標 繩子 錯誤來源的實體
回應資料 物體
輪詢網址 繩子 輪詢 URL 以取得任何進行中提交的狀態
持續提交ID 繩子 任何進行中提交的提交標識碼

範例回應

{
    "isSuccess": true,
    "errors": [{
        "code": "badrequest",
        "message": "Error Message 1",
        "target": "listings"
        }, {
        "code": "warning",
        "message": "Warning Message 1",
        "target": "properties"
    }],
    "responseData": {
        "pollingUrl": "/submission/v1/product/{productId}/submission/{submissionId}/status",
        "ongoingSubmissionId": ""
    } 
}

模組狀態輪詢 API

建立提交之前檢查模組整備程度的 API。 也會驗證套件上傳狀態。

路徑:/submission/v1/product/{productId}/status
方法:GET

路徑參數

名稱 描述
產品ID 產品的合作夥伴中心識別碼

必要標頭

頁首 描述
Authorization: Bearer <Token> 使用向合作夥伴中心帳戶註冊的 Microsoft Entra ID 應用程式識別碼
X-Seller-Account-Id 合作夥伴中心帳戶的賣家識別碼

回應標頭

頁首 描述
X-Correlation-ID 每個請求的 GUID 類型唯一識別碼。 可以與支援團隊分享此資訊以分析任何問題。
Retry-After 由於速率限制,客戶端在再次呼叫 API 之前需要等待的時間 (以秒為單位)。

回應參數

名稱 類型 描述
是否成功 布林值
錯誤 物件陣列 錯誤或警告訊息清單 (如有)
字碼 繩子 訊息的錯誤代碼
訊息 繩子 錯誤的描述
目標 繩子 錯誤來源的實體
回應資料 物體
準備好 布林值 指出所有模組是否處於就緒狀態,包括套件上傳
持續提交ID 繩子 任何進行中提交的提交標識碼

範例回應

{
    "isSuccess": true,
    "errors": [{
        "code": "badrequest",
        "message": "Error Message 1",
        "target": "listings"
        }, {
        "code": "warning",
        "message": "Warning Message 1",
        "target": "properties"
    }],
    "responseData": {
        "isReady": true,
        "ongoingSubmissionId": ""
    }
}

建立提交 API

從 MSI 或 EXE 應用程式的目前草稿建立提交。 API 檢查:

  • 針對作用中提交,如果作用中提交存在,則失敗並出現錯誤訊息。
  • 如果所有模組都處於就緒狀態以建立提交,則為 。
  • 提交中的每個欄位均根據商店的要求進行驗證

路徑:/submission/v1/product/{productId}/submit
方法:POST

路徑參數

名稱 描述
產品ID 產品的合作夥伴中心識別碼

必要標頭

頁首 描述
Authorization: Bearer <Token> 使用向合作夥伴中心帳戶註冊的 Microsoft Entra ID 應用程式識別碼
X-Seller-Account-Id 合作夥伴中心帳戶的賣家識別碼

回應標頭

頁首 描述
X-Correlation-ID 每個請求的 GUID 類型唯一識別碼。 可以與支援團隊分享此資訊以分析任何問題。
Retry-After 由於速率限制,客戶端在再次呼叫 API 之前需要等待的時間 (以秒為單位)。

回應參數

名稱 類型 描述
是否成功 布林值
錯誤 物件陣列 錯誤或警告訊息清單 (如有)
字碼 繩子 訊息的錯誤代碼
訊息 繩子 錯誤的描述
目標 繩子 錯誤來源的實體
回應資料 物體
輪詢網址 繩子 輪詢 URL 以取得模組整備狀態,包括提交套件上傳
提交編號 (submissionId) 繩子 新建立提交的標識碼
持續提交ID 繩子 任何進行中提交的提交標識碼

範例回應

{
    "isSuccess": true,
    "errors": [{
        "code": "badrequest",
        "message": "Error Message 1",
        "target": "listings"
        }, {
        "code": "warning",
        "message": "Warning Message 1",
        "target": "properties"
    }],
    "responseData": {
        "submissionId": "1234567890", 
        "pollingUrl": "/submission/v1/product/{productId}/submission/{submissionId}/status",
        "ongoingSubmissionId": ""
    }
}

提交狀態輪詢 API

檢查提交狀態的 API。

路徑:/submission/v1/product/{productId}/submission/{submissionId}/status
方法:GET

路徑參數

名稱 描述
產品ID 產品的合作夥伴中心識別碼

必要標頭

頁首 描述
Authorization: Bearer <Token> 使用向合作夥伴中心帳戶註冊的 Microsoft Entra ID 應用程式識別碼
X-Seller-Account-Id 合作夥伴中心帳戶的賣家識別碼

回應標頭

頁首 描述
X-Correlation-ID 每個請求的 GUID 類型唯一識別碼。 可以與支援團隊分享此資訊以分析任何問題。
Retry-After 由於速率限制,客戶端在再次呼叫 API 之前需要等待的時間 (以秒為單位)。

回應參數

名稱 類型 描述
是否成功 布林值
錯誤 物件陣列 錯誤或警告訊息清單 (如有)
字碼 繩子 訊息的錯誤代碼
訊息 繩子 錯誤的描述
目標 繩子 錯誤來源的實體
回應資料 物體
出版狀態 繩子 提交狀態 - [INPROGRESS, PUBLISHED, FAILED, UNKNOWN]
已失敗 布林值 指出發佈是否失敗且不會重試

範例回應

{
    "isSuccess": true,
    "errors": [{
        "code": "badrequest",
        "message": "Error Message 1",
        "target": "listings"
        }, {
        "code": "warning",
        "message": "Warning Message 1",
        "target": "properties"
    }],
    "responseData": {
        "publishingStatus": "INPROGRESS",
        "hasFailed": false
    }
}

程式碼範例

下列文章提供詳細的程式碼範例,示範如何以不同的程式設計語言使用 Microsoft Store 提交 API:

C# 範本:MSI 或 EXE 應用程式的 Microsoft Store 提交 API

本文提供 C# 程式碼範例,示範如何使用 MSI 或 EXE 應用程式的 Microsoft Store 提交 API。 您可以檢閱每個範例,以深入了解其中示範的工作,也可以將本文中的所有程式碼範例建置到主控台應用程式。

必要條件 這些範例會使用下列連結庫:

  • 來自 Newtonsoft 的 Newtonsoft.Json NuGet 套件。

主要程式 下列範例會實作命令列程式,呼叫本文中的其他範例方法,以示範使用 Microsoft Store 提交 API 的不同方式。 若要改寫此程式以供您自己使用:

  • 將 SellerId 屬性指派給合作夥伴中心帳戶的賣方標識碼。
  • 將 ApplicationId 屬性指派給您想要管理之應用程式的識別碼。
  • 將 ClientId 和 ClientSecret 屬性指派給應用程式的用戶端識別碼和金鑰,並以您應用程式的租用戶識別碼取代 TokenEndpoint URL 中的 tenantid 字串。 如需詳細資訊,請參閱如何將 Microsoft Entra ID 應用程式與您的合作夥伴中心帳戶產生關聯
using System;
using System.Threading.Tasks;

namespace Win32SubmissionApiCSharpSample
{
    public class Program
    {
        static async Task Main(string[] args)
        {
            var config = new ClientConfiguration()
            {
                ApplicationId = "...",
                ClientId = "...",
                ClientSecret = "...",
                Scope = "https://api.store.microsoft.com/.default",
                ServiceUrl = "https://api.store.microsoft.com",
                TokenEndpoint = "...",
                SellerId = 0
            };

            await new AppSubmissionUpdateSample(config).RunAppSubmissionUpdateSample();

        }
    }
}

使用 C 的 ClientConfiguration 協助程序類別#

範例應用程式會使用 ClientConfiguration 協助程式類別,將 Microsoft Entra ID 目錄資料和應用程式資料傳遞至使用 Microsoft Store 提交 API 的每個範例方法。

using System;
using System.Collections.Generic;
using System.Text;

namespace Win32SubmissionApiCSharpSample
{
    public class ClientConfiguration
    {
        /// <summary>
        /// Client Id of your Microsoft Entra ID Directory app.
        /// Example" 00001111-aaaa-2222-bbbb-3333cccc4444
        /// </summary>
        public string ClientId { get; set; }

        /// <summary>
        /// Client secret of your Microsoft Entra ID Directory app
        /// </summary>
        public string ClientSecret { get; set; }

        /// <summary>
        /// Service root endpoint.
        /// Example: "https://api.store.microsoft.com"
        /// </summary>
        public string ServiceUrl { get; set; }

        /// <summary>
        /// Token endpoint to which the request is to be made. Specific to your Microsoft Entra ID Directory app
        /// Example: https://login.microsoftonline.com/d454d300-128e-2d81-334a-27d9b2baf002/oauth2/v2.0/token
        /// </summary>
        public string TokenEndpoint { get; set; }

        /// <summary>
        /// Resource scope. If not provided (set to null), default one is used for the production API
        /// endpoint ("https://api.store.microsoft.com/.default")
        /// </summary>
        public string Scope { get; set; }

        /// <summary>
        /// Partner Center Application ID.
        /// Example: 3e31a9f9-84e8-4d2d-9eba-487878d02ebf
        /// </summary>
        public string ApplicationId { get; set; }


        /// <summary>
        /// The Partner Center Seller Id
        /// Example: 123456892
        /// </summary>
        public int SellerId { get; set; }
    }
}

使用 C# 建立應用程式提交

下面的範例實作的類別會使用 Microsoft Store 提交 API 中的多個方法來更新應用程式提交。

using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace Win32SubmissionApiCSharpSample
{
    public class AppSubmissionUpdateSample
    {
        private ClientConfiguration ClientConfig;

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="configuration">An instance of ClientConfiguration that contains all parameters populated</param>
        public AppSubmissionUpdateSample(ClientConfiguration configuration)
        {
            this.ClientConfig = configuration;
        }

        /// <summary>
        /// Main method to Run the Sample Application
        /// </summary>
        /// <returns></returns>
        /// <exception cref="InvalidOperationException"></exception>
        public async Task RunAppSubmissionUpdateSample()
        {
            // **********************
            //       SETTINGS
            // **********************
            var appId = this.ClientConfig.ApplicationId;
            var clientId = this.ClientConfig.ClientId;
            var clientSecret = this.ClientConfig.ClientSecret;
            var serviceEndpoint = this.ClientConfig.ServiceUrl;
            var tokenEndpoint = this.ClientConfig.TokenEndpoint;
            var scope = this.ClientConfig.Scope;

            // Get authorization token.
            Console.WriteLine("Getting authorization token");
            var accessToken = await SubmissionClient.GetClientCredentialAccessToken(
                tokenEndpoint,
                clientId,
                clientSecret,
                scope);

            var client = new SubmissionClient(accessToken, serviceEndpoint);

            client.DefaultHeaders = new Dictionary<string, string>()
            {
                {"X-Seller-Account-Id", this.ClientConfig.SellerId.ToString() }
            };

            Console.WriteLine("Getting Current Application Draft Status");
            
            dynamic AppDraftStatus = await client.Invoke<dynamic>(HttpMethod.Get, string.Format(SubmissionClient.ProductDraftStatusPollingUrlTemplate,
                SubmissionClient.Version, appId), null);
            
            Console.WriteLine(AppDraftStatus.ToString());

            Console.WriteLine("Getting Application Packages ");

            dynamic PackagesResponse = await client.Invoke<dynamic>(HttpMethod.Get, string.Format(SubmissionClient.PackagesUrlTemplate,
                SubmissionClient.Version, appId), null);

            Console.WriteLine(PackagesResponse.ToString());

            Console.WriteLine("Getting Single Package");

            dynamic SinglePackageResponse = await client.Invoke<dynamic>(HttpMethod.Get, string.Format(SubmissionClient.PackageByIdUrlTemplate,
                SubmissionClient.Version, appId, (string)PackagesResponse.responseData.packages[0].packageId), null);

            Console.WriteLine(SinglePackageResponse.ToString());

            Console.WriteLine("Updating Entire Package Set");

            // Update data in Packages list to have final set of updated Packages

            // Example - Updating Installer Parameters
            PackagesResponse.responseData.packages[0].installerParameters = "/s /r new-args";

            dynamic PackagesUpdateRequest = new
            {
                packages = PackagesResponse.responseData.packages
            };

            dynamic PackagesUpdateResponse = await client.Invoke<dynamic>(HttpMethod.Put, string.Format(SubmissionClient.PackagesUrlTemplate,
                SubmissionClient.Version, appId), PackagesUpdateRequest);

            Console.WriteLine(PackagesUpdateResponse.ToString());

            Console.WriteLine("Updating Single Package's Download Url");

            // Update data in the SinglePackage object

            var SinglePackageUpdateRequest = SinglePackageResponse.responseData.packages[0];

            // Example - Updating Installer Parameters
            SinglePackageUpdateRequest.installerParameters = "/s /r /t new-args";

            dynamic PackageUpdateResponse = await client.Invoke<dynamic>(HttpMethod.Patch, string.Format(SubmissionClient.PackageByIdUrlTemplate,
                SubmissionClient.Version, appId, SinglePackageUpdateRequest.packageId), SinglePackageUpdateRequest);

            Console.WriteLine("Committing Packages");

            dynamic PackageCommitResponse = await client.Invoke<dynamic>(HttpMethod.Post, string.Format(SubmissionClient.PackagesCommitUrlTemplate,
                SubmissionClient.Version, appId), null);

            Console.WriteLine(PackageCommitResponse.ToString());

            Console.WriteLine("Polling Package Upload Status");

            AppDraftStatus = await client.Invoke<dynamic>(HttpMethod.Get, string.Format(SubmissionClient.ProductDraftStatusPollingUrlTemplate,
                SubmissionClient.Version, appId), null);

            while (!((bool)AppDraftStatus.responseData.isReady))
            {
                AppDraftStatus = await client.Invoke<dynamic>(HttpMethod.Get, string.Format(SubmissionClient.ProductDraftStatusPollingUrlTemplate,
                    SubmissionClient.Version, appId), null);

                Console.WriteLine("Waiting for Upload to finish");

                await Task.Delay(TimeSpan.FromSeconds(2));

                if(AppDraftStatus.errors != null && AppDraftStatus.errors.Count > 0)
                {
                    for(var index = 0; index < AppDraftStatus.errors.Count; index++)
                    {
                        if(AppDraftStatus.errors[index].code == "packageuploaderror")
                        {
                            throw new InvalidOperationException("Package Upload Failed. Please try committing packages again.");
                        }
                    }
                }
            }

            Console.WriteLine("Getting Application Metadata - All Modules");

            dynamic AppMetadata = await client.Invoke<dynamic>(HttpMethod.Get, string.Format(SubmissionClient.AppMetadataUrlTemplate,
                SubmissionClient.Version, appId), null);

            Console.WriteLine(AppMetadata.ToString());

            Console.WriteLine("Getting Application Metadata - Listings");

            dynamic AppListingsMetadata = await client.Invoke<dynamic>(HttpMethod.Get, string.Format(SubmissionClient.AppListingsFetchMetadataUrlTemplate,
                SubmissionClient.Version, appId), null);

            Console.WriteLine(AppListingsMetadata.ToString());

            Console.WriteLine("Updating Listings Metadata - Description");

            // Update Required Fields in Listings Metadata Object - Per Language. For eg. AppListingsMetadata.responseData.listings[0]

            // Example - Updating Description
            AppListingsMetadata.responseData.listings[0].description = "New Description Updated By C# Sample Code";

            dynamic ListingsUpdateRequest = new
            {
                listings = AppListingsMetadata.responseData.listings[0]
            };

            dynamic UpdateListingsMetadataResponse = await client.Invoke<dynamic>(HttpMethod.Put, string.Format(SubmissionClient.AppMetadataUrlTemplate,
                SubmissionClient.Version, appId), ListingsUpdateRequest);

            Console.WriteLine(UpdateListingsMetadataResponse.ToString());

            Console.WriteLine("Getting All Listings Assets");

            dynamic ListingAssets = await client.Invoke<dynamic>(HttpMethod.Get, string.Format(SubmissionClient.ListingAssetsUrlTemplate,
                SubmissionClient.Version, appId), null);

            Console.WriteLine(ListingAssets.ToString());

            Console.WriteLine("Creating Listing Assets for 1 Screenshot");

            
            dynamic AssetCreateRequest = new
            {
                language = ListingAssets.responseData.listingAssets[0].language,
                createAssetRequest = new Dictionary<string, int>()
                {
                    {"Screenshot", 1 },
                    {"Logo", 0 }
                }
            };

            dynamic AssetCreateResponse = await client.Invoke<dynamic>(HttpMethod.Post, string.Format(SubmissionClient.ListingAssetsCreateUrlTemplate,
               SubmissionClient.Version, appId), AssetCreateRequest);

            Console.WriteLine(AssetCreateResponse.ToString());

            Console.WriteLine("Uploading Listing Assets");

            // Path to PNG File to be Uploaded as Screenshot / Logo
            var PathToFile = "./Image.png";
            var AssetToUpload = File.OpenRead(PathToFile);

            await client.UploadAsset(AssetCreateResponse.responseData.listingAssets.screenshots[0].primaryAssetUploadUrl.Value as string, AssetToUpload);

            Console.WriteLine("Committing Listing Assets");

            dynamic AssetCommitRequest = new
            {
                listingAssets = new
                {
                    language = ListingAssets.responseData.listingAssets[0].language,
                    storeLogos = ListingAssets.responseData.listingAssets[0].storeLogos,
                    screenshots = JToken.FromObject(new List<dynamic>() { new
                {
                    id = AssetCreateResponse.responseData.listingAssets.screenshots[0].id.Value as string,
                    assetUrl = AssetCreateResponse.responseData.listingAssets.screenshots[0].primaryAssetUploadUrl.Value as string
                }
                }.ToArray())
                }
            };

            dynamic AssetCommitResponse = await client.Invoke<dynamic>(HttpMethod.Put, string.Format(SubmissionClient.ListingAssetsCommitUrlTemplate,
               SubmissionClient.Version, appId), AssetCommitRequest);

            Console.WriteLine(AssetCommitResponse.ToString());

            Console.WriteLine("Getting Current Application Draft Status before Submission");

            AppDraftStatus = await client.Invoke<dynamic>(HttpMethod.Get, string.Format(SubmissionClient.ProductDraftStatusPollingUrlTemplate,
                SubmissionClient.Version, appId), null);

            Console.WriteLine(AppDraftStatus.ToString());

            if (AppDraftStatus == null || !((bool)AppDraftStatus.responseData.isReady))
            {
                throw new InvalidOperationException("Application Current Status is not in Ready Status for All Modules");
            }

            Console.WriteLine("Creating Submission");

            dynamic SubmissionCreationResponse = await client.Invoke<dynamic>(HttpMethod.Post, string.Format(SubmissionClient.CreateSubmissionUrlTemplate,
                SubmissionClient.Version, appId), null);

            Console.WriteLine(SubmissionCreationResponse.ToString());

            Console.WriteLine("Current Submission Status");

            dynamic SubmissionStatus = await client.Invoke<dynamic>(HttpMethod.Get, string.Format(SubmissionClient.SubmissionStatusPollingUrlTemplate,
                SubmissionClient.Version, appId, SubmissionCreationResponse.responseData.submissionId.Value as string), null);

            Console.Write(SubmissionStatus.ToString());

            // User can Poll on this API to know if Submission Status is INPROGRESS, PUBLISHED or FAILED.
            // This Process involves File Scanning, App Certification and Publishing and can take more than a day.
        }
    }
}

使用 C# 的 IngestionClient 幫助程式類

IngestionClient 類別會提供協助程式方法,供範例應用程式中的其他方法用來執行下列工作:

  • 取得可用來呼叫 Microsoft Store 提交 API 中的方法的 Microsoft Entra ID 存取權杖。 取得權杖之後,您有 60 分鐘的時間使用此權杖來呼叫 Microsoft Store 提交 API,之後權杖才會到期。 權杖過期後,您可以生成新的權杖。
  • 處理 Microsoft Store 提交 API 的 HTTP 要求。
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;

namespace Win32SubmissionApiCSharpSample
{
    /// <summary>
    /// This class is a proxy that abstracts the functionality of the API service
    /// </summary>
    public class SubmissionClient : IDisposable
    {
        public static readonly string Version = "1";
        private HttpClient httpClient;
        private HttpClient imageUploadClient;

        private readonly string accessToken;

        public static readonly string PackagesUrlTemplate = "/submission/v{0}/product/{1}/packages";
        public static readonly string PackageByIdUrlTemplate = "/submission/v{0}/product/{1}/packages/{2}";
        public static readonly string PackagesCommitUrlTemplate = "/submission/v{0}/product/{1}/packages/commit";
        public static readonly string AppMetadataUrlTemplate = "/submission/v{0}/product/{1}/metadata";
        public static readonly string AppListingsFetchMetadataUrlTemplate = "/submission/v{0}/product/{1}/metadata/listings";
        public static readonly string ListingAssetsUrlTemplate = "/submission/v{0}/product/{1}/listings/assets";
        public static readonly string ListingAssetsCreateUrlTemplate = "/submission/v{0}/product/{1}/listings/assets/create";
        public static readonly string ListingAssetsCommitUrlTemplate = "/submission/v{0}/product/{1}/listings/assets/commit";
        public static readonly string ProductDraftStatusPollingUrlTemplate = "/submission/v{0}/product/{1}/status";
        public static readonly string CreateSubmissionUrlTemplate = "/submission/v{0}/product/{1}/submit";
        public static readonly string SubmissionStatusPollingUrlTemplate = "/submission/v{0}/product/{1}/submission/{2}/status";

        public const string JsonContentType = "application/json";
        public const string PngContentType = "image/png";
        public const string BinaryStreamContentType = "application/octet-stream";

        /// <summary>
        /// Initializes a new instance of the <see cref="SubmissionClient" /> class.
        /// </summary>
        /// <param name="accessToken">
        /// The access token. This is JWT a token obtained from Microsoft Entra ID Directory allowing the caller to invoke the API
        /// on behalf of a user
        /// </param>
        /// <param name="serviceUrl">The service URL.</param>
        public SubmissionClient(string accessToken, string serviceUrl)
        {
            if (string.IsNullOrEmpty(accessToken))
            {
                throw new ArgumentNullException("accessToken");
            }

            if (string.IsNullOrEmpty(serviceUrl))
            {
                throw new ArgumentNullException("serviceUrl");
            }

            this.accessToken = accessToken;
            this.httpClient = new HttpClient
            {
                BaseAddress = new Uri(serviceUrl)
            };
            this.imageUploadClient = new HttpClient();
            this.DefaultHeaders = new Dictionary<string, string>();
        }

        /// <summary>
        /// Gets or Sets the default headers.
        /// </summary>
        public Dictionary<string, string> DefaultHeaders { get; set; }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting
        /// unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            if (this.httpClient != null)
            {
                this.httpClient.Dispose();
                this.httpClient = null;
                GC.SuppressFinalize(this);
            }
        }

        /// <summary>
        /// Gets the authorization token for the provided client id, client secret, and the scope.
        /// This token is usually valid for 1 hour, so if your submission takes longer than that to complete,
        /// make sure to get a new one periodically.
        /// </summary>
        /// <param name="tokenEndpoint">Token endpoint to which the request is to be made. Specific to your
        /// Microsoft Entra ID Directory app. Example: https://login.microsoftonline.com/d454d300-128e-2d81-334a-27d9b2baf002/oauth2/v2.0/token </param>
        /// <param name="clientId">Client Id of your Microsoft Entra ID Directory app. Example" 00001111-aaaa-2222-bbbb-3333cccc4444</param>
        /// <param name="clientSecret">Client secret of your Microsoft Entra ID Directory app</param>
        /// <param name="scope">Scope. If not provided, default one is used for the production API endpoint.</param>
        /// <returns>Autorization token. Prepend it with "Bearer: " and pass it in the request header as the
        /// value for "Authorization: " header.</returns>
        public static async Task<string> GetClientCredentialAccessToken(
            string tokenEndpoint,
            string clientId,
            string clientSecret,
            string scope = null)
        {
            if (scope == null)
            {
                scope = "https://api.store.microsoft.com/.default";
            }

            dynamic result;
            using (HttpClient client = new HttpClient())
            {
                string tokenUrl = tokenEndpoint;
                using (
                    HttpRequestMessage request = new HttpRequestMessage(
                        HttpMethod.Post,
                        tokenUrl))
                {
                    string strContent =
                        string.Format(
                            "grant_type=client_credentials&client_id={0}&client_secret={1}&scope={2}",
                            clientId,
                            clientSecret,
                            scope);

                    request.Content = new StringContent(strContent, Encoding.UTF8,
                        "application/x-www-form-urlencoded");

                    using (HttpResponseMessage response = await client.SendAsync(request))
                    {
                        string responseContent = await response.Content.ReadAsStringAsync();
                        result = JsonConvert.DeserializeObject(responseContent);
                    }
                }
            }

            return result.access_token;
        }


        /// <summary>
        /// Invokes the specified HTTP method.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="httpMethod">The HTTP method.</param>
        /// <param name="relativeUrl">The relative URL.</param>
        /// <param name="requestContent">Content of the request.</param>
        /// <returns>instance of the type T</returns>
        /// <exception cref="ServiceException"></exception>
        public async Task<T> Invoke<T>(HttpMethod httpMethod,
            string relativeUrl,
            object requestContent)
        {
            using (var request = new HttpRequestMessage(httpMethod, relativeUrl))
            {
                this.SetRequest(request, requestContent);

                using (HttpResponseMessage response = await this.httpClient.SendAsync(request))
                {
                    T result;
                    if (this.TryHandleResponse(response, out result))
                    {
                        return result;
                    }

                    if (response.IsSuccessStatusCode)
                    {
                        var resource = JsonConvert.DeserializeObject<T>(await response.Content.ReadAsStringAsync());
                        return resource;
                    }

                    throw new Exception(await response.Content.ReadAsStringAsync());
                }
            }
        }

        /// <summary>
        /// Uploads a given Image Asset file to Asset Storage
        /// </summary>
        /// <param name="assetUploadUrl">Asset Storage Url</param>
        /// <param name="fileStream">The Stream instance of file to be uploaded</param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        public async Task UploadAsset(string assetUploadUrl, Stream fileStream)
        {
            using (var request = new HttpRequestMessage(HttpMethod.Put, assetUploadUrl))
            {
                request.Headers.Add("x-ms-blob-type", "BlockBlob");
                request.Content = new StreamContent(fileStream);
                request.Content.Headers.ContentType = new MediaTypeHeaderValue(PngContentType);
                using (HttpResponseMessage response = await this.imageUploadClient.SendAsync(request))
                {
                    if (response.IsSuccessStatusCode)
                    {
                        return;
                    }
                    throw new Exception(await response.Content.ReadAsStringAsync());
                }
            }
        }

        /// <summary>
        /// Sets the request.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <param name="requestContent">Content of the request.</param>
        protected virtual void SetRequest(HttpRequestMessage request, object requestContent)
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", this.accessToken);

            foreach (var header in this.DefaultHeaders)
            {
                request.Headers.Add(header.Key, header.Value);
            }

            if (requestContent != null)
            {
                request.Content = new StringContent(JsonConvert.SerializeObject(requestContent),
                        Encoding.UTF8,
                        JsonContentType);
                
            }
        }


        /// <summary>
        /// Tries the handle response.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="response">The response.</param>
        /// <param name="result">The result.</param>
        /// <returns>true if the response was handled</returns>
        protected virtual bool TryHandleResponse<T>(HttpResponseMessage response, out T result)
        {
            result = default(T);
            return false;
        }
    }
}

Node.js 範本:MSI 或 EXE 應用程式的 Microsoft Store 提交 API

本文提供 Node.js 程式碼範例,示範如何使用 MSI 或 EXE 應用程式的 Microsoft Store 提交 API。 您可以檢閱每個範例,以深入了解其中示範的工作,也可以將本文中的所有程式碼範例建置到主控台應用程式。

必要條件 這些範例會使用下列連結庫:

  • node-fetch v2 [使用 npm 安裝 node-fetch@2]

使用 node.js 建立應用程式提交

下列範例會呼叫本文中的其他範例方法,以示範使用 Microsoft Store 提交 API 的不同方式。 若要改寫此程式以供您自己使用:

  • 將 SellerId 屬性指派給合作夥伴中心帳戶的賣方標識碼。
  • 將 ApplicationId 屬性指派給您想要管理之應用程式的識別碼。
  • 將 ClientId 和 ClientSecret 屬性指派給應用程式的用戶端識別碼和金鑰,並以您應用程式的租用戶識別碼取代 TokenEndpoint URL 中的 tenantid 字串。 如需詳細資訊,請參閱如何將 Microsoft Entra ID 應用程式與您的合作夥伴中心帳戶產生關聯

下面的範例實作的類別會使用 Microsoft Store 提交 API 中的多個方法來更新應用程式提交。

const config = require('./Configuration');
const submissionClient = require('./SubmissionClient');
const fs = require('fs');

var client = new submissionClient(config);

/**
 * Main entry method to Run the Store Submission API Node.js Sample
 */
async function RunNodeJsSample(){
    print('Getting Access Token');
    await client.getAccessToken();
    
    print('Getting Current Application Draft Status');
    var currentDraftStatus = await client.callStoreAPI(client.productDraftStatusPollingUrlTemplate, 'get');
    print(currentDraftStatus);

    print('Getting Application Packages');
    var currentPackages = await client.callStoreAPI(client.packagesUrlTemplate, 'get');
    print(currentPackages);

    print('Getting Single Package');
    var packageId = currentPackages.responseData.packages[0].packageId;
    var packageIdUrl = `${client.packageByIdUrlTemplate}`.replace('{packageId}', packageId);
    var singlePackage = await client.callStoreAPI(packageIdUrl, 'get');
    print(singlePackage);

    print('Updating Entire Package Set');
    // Update data in Packages list to have final set of updated Packages
    currentPackages.responseData.packages[0].installerParameters = "/s /r new-args";
    var packagesUpdateRequest = {
        'packages': currentPackages.responseData.packages
    };
    print(packagesUpdateRequest);
    var packagesUpdateResponse = await client.callStoreAPI(client.packagesUrlTemplate, 'put', packagesUpdateRequest);
    print(packagesUpdateResponse);

    print('Updating Single Package\'s Download Url');
    // Update data in the SinglePackage object
    singlePackage.responseData.packages[0].installerParameters = "/s /r /t new-args";
    var singlePackageUpdateResponse = await client.callStoreAPI(packageIdUrl, 'patch', singlePackage.responseData.packages[0]);
    print(singlePackageUpdateResponse);

    print('Committing Packages');
    var commitPackagesResponse = await client.callStoreAPI(client.packagesCommitUrlTemplate, 'post');
    print(commitPackagesResponse);

    await poll(async ()=>{
        print('Waiting for Upload to finish');
        return await client.callStoreAPI(client.productDraftStatusPollingUrlTemplate, 'get');
    }, 2);

    print('Getting Application Metadata - All Modules');
    var appMetadata = await client.callStoreAPI(client.appMetadataUrlTemplate, 'get');
    print(appMetadata);

    print('Getting Application Metadata - Listings');
    var appListingMetadata = await client.callStoreAPI(client.appListingsFetchMetadataUrlTemplate, 'get');
    print(appListingMetadata);

    print('Updating Listings Metadata - Description');   
    // Update Required Fields in Listings Metadata Object - Per Language. For eg. AppListingsMetadata.responseData.listings[0]
    // Example - Updating Description
    appListingMetadata.responseData.listings[0].description = 'New Description Updated By Node.js Sample Code';
    var listingsUpdateRequest = {
        'listings': appListingMetadata.responseData.listings[0]
    };
    var listingsMetadataUpdateResponse = await client.callStoreAPI(client.appMetadataUrlTemplate, 'put', listingsUpdateRequest);
    print(listingsMetadataUpdateResponse);

    print('Getting All Listings Assets');
    var listingAssets = await client.callStoreAPI(client.listingAssetsUrlTemplate, 'get');
    print(listingAssets);

    print('Creating Listing Assets for 1 Screenshot');
    var listingAssetCreateRequest = {
        'language': listingAssets.responseData.listingAssets[0].language,
        'createAssetRequest': {
            'Screenshot': 1,
            'Logo': 0
        }
    };
    var listingAssetCreateResponse = await client.callStoreAPI(client.listingAssetsCreateUrlTemplate, 'post', listingAssetCreateRequest);
    print(listingAssetCreateResponse);

    print('Uploading Listing Assets');
    const pathToFile = './Image.png';
    const stats = fs.statSync(pathToFile);
    const fileSize = stats.size;
    const fileStream = fs.createReadStream(pathToFile);
    await client.uploadAssets(listingAssetCreateResponse.responseData.listingAssets.screenshots[0].primaryAssetUploadUrl, fileStream, fileSize);

    print('Committing Listing Assets');
    var assetCommitRequest = {
        'listingAssets': {
            'language': listingAssets.responseData.listingAssets[0].language,
            'storeLogos': listingAssets.responseData.listingAssets[0].storeLogos,
            'screenshots': [{
                'id': listingAssetCreateResponse.responseData.listingAssets.screenshots[0].id,
                'assetUrl': listingAssetCreateResponse.responseData.listingAssets.screenshots[0].primaryAssetUploadUrl
            }]
        }
    };
    var assetCommitResponse = await client.callStoreAPI(client.listingAssetsCommitUrlTemplate, 'put', assetCommitRequest);
    print(assetCommitResponse);

    print('Getting Current Application Draft Status before Submission');
    currentDraftStatus = await client.callStoreAPI(client.productDraftStatusPollingUrlTemplate, 'get');
    print(currentDraftStatus);
    if(!currentDraftStatus.responseData.isReady){
        throw new Error('Application Current Status is not in Ready Status for All Modules');
    }

    print('Creating Submission');
    var submissionCreationResponse = await client.callStoreAPI(client.createSubmissionUrlTemplate, 'post');
    print(submissionCreationResponse);

    print('Current Submission Status');
    var submissionStatusUrl = `${client.submissionStatusPollingUrlTemplate}`.replace('{submissionId}', submissionCreationResponse.responseData.submissionId);
    var submissionStatusResponse = await client.callStoreAPI(submissionStatusUrl, 'get');
    print(submissionStatusResponse);

    // User can Poll on this API to know if Submission Status is INPROGRESS, PUBLISHED or FAILED.
    // This Process involves File Scanning, App Certification and Publishing and can take more than a day.
}

/**
 * Utility Method to Poll using a given function and time interval in seconds
 * @param {*} func 
 * @param {*} intervalInSeconds 
 * @returns 
 */
async function poll(func, intervalInSeconds){
var result = await func();
if(result.responseData.isReady){
    Promise.resolve(true);
}
else if(result.errors && result.errors.length > 0 && result.errors.find(element => element.code == 'packageuploaderror') != undefined){
throw new Error('Package Upload Failed');
}
else{
    await new Promise(resolve => setTimeout(resolve, intervalInSeconds*1000));
    return await poll(func, intervalInSeconds); 
}
}

/**
 * Utility function to Print a Json or normal string
 * @param {*} json 
 */
function print(json){
    if(typeof(json) == 'string'){
        console.log(json);
    }
    else{
        console.log(JSON.stringify(json));
    }
    console.log("\n");
}

/** Run the Node.js Sample Application */
RunNodeJsSample();

ClientConfiguration 協助程式

範例應用程式會使用 ClientConfiguration 協助程式類別,將 Microsoft Entra ID 目錄資料和應用程式資料傳遞至使用 Microsoft Store 提交 API 的每個範例方法。

/** Configuration Object for Store Submission API */
var config = {
    version : "1",
    applicationId : "...",
    clientId : "...",
    clientSecret : "...",
    serviceEndpoint : "https://api.store.microsoft.com",
    tokenEndpoint : "...",
    scope : "https://api.store.microsoft.com/.default",
    sellerId : "...",
    jsonContentType : "application/json",
    pngContentType : "image/png",
    binaryStreamContentType : "application/octet-stream"
};

module.exports = config;

使用 node.js的 IngestionClient 協助程式

IngestionClient 類別會提供協助程式方法,供範例應用程式中的其他方法用來執行下列工作:

  • 取得可用來呼叫 Microsoft Store 提交 API 中的方法的 Microsoft Entra ID 存取權杖。 取得權杖之後,您有 60 分鐘的時間使用此權杖來呼叫 Microsoft Store 提交 API,之後權杖才會到期。 權杖過期後,您可以生成新的權杖。
  • 處理 Microsoft Store 提交 API 的 HTTP 要求。
const fetch = require('node-fetch');
/**
 * Submission Client to invoke all available Store Submission API and Asset Upload to Blob Store
 */
class SubmissionClient{

    constructor(config){
        this.configuration = config;
        this.accessToken = "";
        this.packagesUrlTemplate = `/submission/v${this.configuration.version}/product/${this.configuration.applicationId}/packages`;
        this.packageByIdUrlTemplate = `/submission/v${this.configuration.version}/product/${this.configuration.applicationId}/packages/{packageId}`;
        this.packagesCommitUrlTemplate = `/submission/v${this.configuration.version}/product/${this.configuration.applicationId}/packages/commit`;
        this.appMetadataUrlTemplate = `/submission/v${this.configuration.version}/product/${this.configuration.applicationId}/metadata`;
        this.appListingsFetchMetadataUrlTemplate = `/submission/v${this.configuration.version}/product/${this.configuration.applicationId}/metadata/listings`;
        this.listingAssetsUrlTemplate = `/submission/v${this.configuration.version}/product/${this.configuration.applicationId}/listings/assets`;
        this.listingAssetsCreateUrlTemplate = `/submission/v${this.configuration.version}/product/${this.configuration.applicationId}/listings/assets/create`;
        this.listingAssetsCommitUrlTemplate = `/submission/v${this.configuration.version}/product/${this.configuration.applicationId}/listings/assets/commit`;
        this.productDraftStatusPollingUrlTemplate = `/submission/v${this.configuration.version}/product/${this.configuration.applicationId}/status`;
        this.createSubmissionUrlTemplate = `/submission/v${this.configuration.version}/product/${this.configuration.applicationId}/submit`;
        this.submissionStatusPollingUrlTemplate = `/submission/v${this.configuration.version}/product/${this.configuration.applicationId}/submission/{submissionId}/status`;
    }
    
    async getAccessToken(){
        var params = new URLSearchParams();
        params.append('grant_type','client_credentials');
        params.append('client_id',this.configuration.clientId);
        params.append('client_secret',this.configuration.clientSecret);
        params.append('scope',this.configuration.scope);
        var response = await fetch(this.configuration.tokenEndpoint,{
            method: "POST",
            body: params
        });    
        var data = await response.json();
        this.accessToken = data.access_token;
    }

    async callStoreAPI(url, method, data){
        var request = {
            method: method,
            headers:{
                'Authorization': `Bearer ${this.accessToken}`,
                'Content-Type': this.configuration.jsonContentType,
                'X-Seller-Account-Id': this.configuration.sellerId
            },            
        };
        if(data){
            request.body = JSON.stringify(data);
        }
        var response = await fetch(`${this.configuration.serviceEndpoint}${url}`,request);
        var jsonResponse = await response.json();
        return jsonResponse;
    }

    async uploadAssets(url, stream, size){
        var request = {
            method: 'put',
            headers:{
                'Content-Type': this.configuration.pngContentType,
                'x-ms-blob-type': 'BlockBlob',
                "Content-length": size
            },            
            body: stream
        };
        var response = await fetch(`${url}`,request);
        if(response.ok){
            return response;
        }
        else{
            throw new Error('Uploading of assets failed');
        }
    }
}
module.exports = SubmissionClient;

其他說明

如果您有關於 Microsoft Store 提交 API 的疑問,或需要使用此 API 管理提交方面的協助,請使用下列資源:

  • 我們的論壇上提問。
  • 請瀏覽我們的支援頁面,並要求合作夥伴中心的其中一個協助支援選項。 如果系統提示您選擇問題類型和類別,請分別選擇 [應用程式提交和認證] 和 [提交應用程式]。