适用于 MSI 或 EXE 应用的 Microsoft Store 提交 API

使用 适用于 MSI 或 EXE 应用的 Microsoft Store 提交 API 以编程方式查询,并为你的或组织的合作伙伴中心帐户的应用、加载项和软件包外部测试版创建提交。 当你的帐户管理着多个应用或加载项,而你想要自动执行并优化这些资源的提交过程时,此 API 十分实用。 此 API 使用 Azure Active Directory (Azure AD) 验证来自应用或服务的调用。

以下步骤介绍了使用 Microsoft Store 提交 API 的端到端过程:

  1. 确保已完成所有先决条件。
  2. 在调用 Microsoft Store 提交 API 中的方法之前,请先获取 Azure AD 访问令牌。 获取令牌后,可以在 60 分钟的令牌有效期内,使用该令牌调用“Microsoft Store 提交 API”。 该令牌到期后,可以重新生成一个。
  3. 调用适用于 MSI 或 EXE 应用的 Microsoft Store 提交 API。

步骤 1:完成使用 Microsoft Store 提交 API 的先决条件

在开始编写调用 适用于 MSI 或 EXE 应用的 Microsoft Store 提交 API 的代码之前,请确保已完成以下先决条件。

  • 你(或你的组织)必须有一个 Azure AD 目录,并且必须对该目录拥有全局管理员权限。 如果你已使用 Microsoft 365 或 Microsoft 的其他业务服务,表示你已经具有 Azure AD 目录。 否则,你可以免费在合作伙伴中心中创建新的 Azure AD
  • 必须将某个 Azure AD 应用程序与你的合作伙伴中心帐户相关联,并获取租户 ID、客户端 ID 和密钥。 需要使用这些值来获取 Azure AD 访问令牌,调用“Microsoft Store 提交 API”时将会使用该令牌。
  • 为你的应用做好使用 Microsoft Store 提交 API 的准备:

如何将 Azure AD 应用程序与合作伙伴中心帐户相关联

在使用适用于 MSI 或 EXE 应用的 Microsoft Store 提交 API 前,请先将 Azure AD 应用程序关联你的合作伙伴中心帐户、检索该应用程序的租户 ID 和客户端 ID并生成密钥。 Azure AD 应用程序是指要调用 Microsoft Store 提交 API 的应用或服务。 需要使用该租户 ID、客户端 ID 和密钥来获取要传递给 API 的 Azure AD 访问令牌。

注意

你只需执行一次此任务。 获取租户 ID、客户端 ID 和密钥后,每当需要创建新的 Azure AD 访问令牌时,都可以重复使用它们。

  1. 在合作伙伴中心,将组织的合作伙伴中心帐户与组织的 Azure AD 目录相关联
  2. 接下来,在合作伙伴中心的用户页的帐户设置部分添加 Azure AD 应用程序,即你要以合作伙伴中心帐户访问提交的应用或服务。 确保为此应用程序分配“管理者”角色。 如果你的 Azure AD 目录中尚不包含该应用程序,可以在合作伙伴中心创建新的 Azure AD 应用程序
  3. 返回到“用户”页,单击 Azure AD 应用程序的名称转到“应用程序设置”,然后复制“租户 ID”和“客户端 ID”值。
  4. 要添加新密钥或客户端密码,请参阅以下说明,或参考通过 Azure 门户注册应用的说明

要注册应用,请执行以下操作:

  1. 登录到 Azure 门户

  2. 如果有权访问多个租户,请使用顶部菜单中的“目录 + 订阅”筛选器 ,以切换到要在其中注册应用程序的租户。

  3. 搜索并选择“Azure Active Directory”。

  4. 在“管理”下,选择“应用注册”>,然后选择你的应用程序 。

  5. 选择“证书和机密”>“客户端密码”>“新建客户端密码”。

  6. 添加客户端机密的说明。

  7. 选择机密的过期时间,或指定自定义的生存期。

  8. 客户端机密生存期限制为两年(24 个月)或更短。 不能指定超过 24 个月的自定义生存期。

    注意

    Microsoft 建议将过期时间值设置为小于 12 个月。

  9. 选择 添加 。

  10. 记录机密的值,以便在客户端应用程序代码中使用。 退出此页面后,此机密值永不再显示。

步骤 2:获取 Azure AD 访问令牌

在调用 适用于 MSI 或 EXE 应用的 Microsoft Store 提交 API 的方法前,必须先获取 Azure AD 访问令牌,并传递给 API 各方法的授权标头。 获取访问令牌后,你有 60 分钟的时间来使用它,60 分钟后它将过期。 该令牌到期后,可以对它进行刷新,以便可以在之后调用该 API 时继续使用。

要获取访问令牌,请按照 [Service to Service Calls Using Client Credentials]/azure/active-directory/azuread-dev/v1-oauth2-client-creds-grant-flow) 中的说明将 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

请为 POST URI 中的 tenant_id 值,以及 client_idclient_secret 参数指定在上一节中从合作伙伴中心获取的应用程序租户 ID、客户端 ID 和密钥。 必须指定 scope 参数的 https://api.store.microsoft.com/.default

在你的访问令牌到期后,你可按照此处的说明刷新令牌。

有关演示如何使用 C# 或 Node.js 获取访问令牌的示例,请参阅适用于 MSI 或 EXE 应用的 Microsoft Store 提交 API 代码示例

第 3 步:使用Microsoft Store 提交 API

获取 Azure AD 访问令牌后,才能调用适用于 MSI 或 EXE 应用的 Microsoft Store 提交 API 中的方法。 该 API 的许多方法已经按应用的场景分类。 要创建或更新提交,一般需按特定顺序调用多个方法。 有关各场景中每个方法的语法信息,请参见以下节:

注意

获取访问令牌后,可以在 60 分钟的令牌有效期内,使用该令牌调用 适用于 MSI 或 EXE 应用的 Microsoft Store 提交 API。

基 URL

适用于 MSI 或 EXE 应用的 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

路径参数

参数 说明
productId 产品的合作伙伴中心 ID
moduleName 合作伙伴中心模块 - 列表、属性或可用性

查询参数

参数 说明
语言 可选 :列表语言筛选器使用逗号分隔字符串 [最多 200 种语言]。

如果空置,将提取前 200 个可用的列表语言元数据。 [例如“en-us, en-gb"]。
includelanguagelist 可选 布尔值 – 如果为 true,则返回添加的列表语言及其完整性状态的列表。

必需标头

标头
Authorization: Bearer <Token> 以合作伙伴中心帐户注册的 Azure AD 应用 ID
X-Seller-Account-Id 合作伙伴中心帐户的卖家 ID

响应头

标头
X-Correlation-ID 每个请求都应该使用唯一的 GUID。 可以将其与支持团队共享以用于分析问题。
Retry-After 由于速率限制,客户端在再次调用 API 之前需要等待的时间(以秒为单位)。

响应参数

名称 Type 描述
accessibilitySupport 布尔
additionalLicenseTerms 字符串
availability Object 可用性模块数据
category 字符串 请参阅下面的类别列表
certificationNotes 字符串
code 字符串 消息的错误代码
ContactInfo 字符串
copyright 字符串
dependsOnDriversOrNT 布尔
description 字符串
developedBy 字符串
discoverability 字符串 [DISCOVERABLE, DEEPLINK_ONLY]
enableInFutureMarkets 布尔
errors 对象数组 错误或警告消息列表(如果有)
FreeTrial 字符串 [NO_FREE_TRIAL, FREE_TRIAL]
hardwareItemType 字符串
isPrivacyPolicyRequired 布尔
isRecommended 布尔
IsRequired 布尔
isSuccess 布尔
isSystemFeatureRequired 对象数组
language 字符串 查看语言列表
listings 对象数组 列出每种语言的模块数据
markets 字符串数组 请参阅下面的市场列表
message 字符串 对错误的说明
minimumHardware 字符串
minimumRequirement 字符串
penAndInkSupport 布尔
定价 字符串 [FREE, FREEMIUM, SUBSCRIPTION, PAID]
privacyPolicyUrl 字符串
productDeclarations Object
ProductFeatures 字符串数组
properties Object 属性模块数据
recommendedHardware 字符串
recommendedRequirement 字符串
responseData Object 包含请求的实际响应有效负载
要求  对象数组
SearchTerms 字符串数组
shortDescription 字符串
子类别 字符串 请参阅下面的子类别列表
supportContactInfo 字符串
systemRequirementDetails 对象数组
target 字符串 发生错误的实体
网站 字符串
whatsNew 字符串

示例响应

{
    "isSuccess": true,
    "errors": [{
        "code": "badrequest",
        "message": "Error Message 1",
        "target": "listings"
        }, {
        "code": "warning",
        "message": "Warning Message 1",
        "target": "properties"
    }],
    "responseData": {
        "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"               
        }],      
        "listingLanguages": [{"language":"en-us", "isComplete": true}]
    }
}

更新当前草稿提交元数据 API

更新草稿提交中所有模块的元数据。 API 检查

  • 活动的提交。 如果存在,则失败并显示错误消息。
  • 所有模块是否都处于允许保存草稿操作的就绪状态。
  • 是否已根据应用商店的要求对提交中的每个字段进行验证
  • 系统需求详请的验证规则:
    • hardwareItemType 中的允许值 = Memory: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时,只有要更新的字段才需要出现在请求中。 请求到的字段值将覆盖其现有值,其他所有未在请求中出现的字段,将与该模块的当前字段相同。

路径参数

参数 说明
productId 产品的合作伙伴中心 ID

必需标头

标头
Authorization: Bearer <Token> 以合作伙伴中心帐户注册的 Azure AD 应用 ID
X-Seller-Account-Id 合作伙伴中心帐户的卖家 ID

请求参数

名称 Type 说明
availability Object 用于保存可用性模块元数据的对象
markets 字符串数组 必需 参见下面的市场列表
discoverability 字符串 必需 [DISCOVERABLE, DEEPLINK_ONLY]
enableInFutureMarkets 布尔 必需
定价 字符串 Required [FREE, FREEMIUM, SUBSCRIPTION, PAID]
FreeTrial 字符串 当 Princing 为 PAID 或 SUBSCRIPTION 时必需 [NO_FREE_TRIAL, FREE_TRIAL]
properties Object 用于保存属性模块元数据的对象
isPrivacyPolicyRequired 布尔 必需
privacyPolicyUrl 字符串 isPrivacyPolicyRequired = true 时必需 必须为有效的 URL
网站 字符串 必须是有效的 URI
supportContactInfo 字符串 必须为有效的 URL 或电子邮件地址
certificationNotes 字符串 建议 字符限制 = 2000
category 字符串 必需 请参阅下面的类别列表
子类别 字符串 必需 请参阅下面的子类别列表
productDeclarations Object 必需
isSystemFeatureRequired 对象数组 [Touch, Keyboard, Mouse, Camera, NFC_HCE, NFC_Proximity, Bluetooth_LE, Telephony, Microphone]
IsRequired 布尔 必需
isRecommended 布尔 必需
hardwareItemType 字符串 必需
systemRequirementDetails 对象数组 [Processor, Graphics, Memory, DirectX, Video_Memory]
minimumRequirement 字符串 必需 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]
recommendedRequirement 字符串 必需 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]
dependsOnDriversOrNT 布尔 必需
accessibilitySupport 布尔 必需
penAndInkSupport 布尔 必需
listings Object 用于列出单语言模块数据的对象
language 字符串 必需 请参阅以下语言列表
description 字符串 必需 字符限制 = 10000
whatsNew 字符串 字符限制:1500
ProductFeatures 字符串数组 每个功能 200 个字符;最多 20 个功能
shortDescription 字符串 字符限制:1000
SearchTerms 字符串数组 每个搜索词 30 个字符;最多 7 个搜索词

所有搜索词中共 21 个不重复单词
additionalLicenseTerms 字符串 必需 字符限制 = 10000
copyright 字符串 字符限制 = 200
developedBy 字符串 字符限制 = 255
要求  对象数组 每项 200 个字符;在最小值和推荐值之间共计不超过 11 项]
minimumHardware 字符串 字符限制 = 200
recommendedHardware 字符串 字符限制 = 200
ContactInfo 字符串 字符限制 = 200
listingsToAdd 字符串数组 查看语言列表
listingsToRemove 字符串数组 查看语言列表

市场

市场 缩写
阿富汗 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
Tog - TG
托克劳 TK
汤加 TO
特立尼达和多巴哥
突尼斯 TN
土耳其 TR
土库曼斯坦 TM
特克斯和凯科斯群岛 TC
图瓦卢 电视
美国美属外岛 UM
美国维尔京群岛 VI
乌干达 UG
乌克兰 UA
阿拉伯联合酋长国 AE
英国 GB
United States US
乌拉圭 UY
乌兹别克斯坦 UZ
瓦努阿图 VU
越南 VN
瓦利斯和富图纳 WF
也门 YE
赞比亚 ZM
津巴布韦 ZW
奥兰群岛 AX

类别和子类别

类别 子类别
BooksAndReference EReader、Fiction、Nonfiction、Reference
业务 AccountingAndfinance、Collaboration、CRM、DataAndAnalytics、FileManagement、InventoryAndlogistics、LegalAndHR、ProjectManagement、RemoteDesktop、SalesAndMarketing、TimeAndExpenses
DeveloperTools Database、DesignTools、DevelopmentKits、Networking、ReferenceAndTraining、Servers、Utilities、WebHosting
教育 EducationBooksAndReference、EarlyLearning、InstructionalTools、Language、StudyAids
娱乐 (无)
FoodAndDining (无)
GovernmentAndPolitics (无)
HealthAndFitness (无)
KidsAndFamily KidsAndFamilyBooksAndReference、KidsAndFamilyEntertainment、HobbiesAndToys、SportsAndActivities、KidsAndFamilyTravel
生活 Automotive、DYI、HomeAndGarden、Relationships、SpecialInterest、StyleAndFashion
医疗 (无)
MultimediaDesign IllustrationAndGraphicDesign, 音乐Production, PhotoAndVideoProduction
Music (无)
NavigationAndMaps (无)
NewsAndWeather News,Weather
PersonalFinance BankingAndInvestments、BudgetingAndTaxes
个性化 RingtonesAndSounds、themes、WallpaperAndLockScreens
PhotoAndVideo (无)
工作效率 (无)
安全性 PCProtection、PersonalSecurity
购物 (无)
社交 (无)
体育游戏 (无)
旅行 CityGuides、Hotels
UtilitiesAndTools BackupAndManage、FileManager

语言

语言名称 支持的语言代码
南非荷兰语 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
Bangla 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、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
Hindi hi, hi-in
匈牙利语 hu, hu-hu
冰岛语 is, is-is
Igb - 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 之前需要等待的时间(以秒为单位)

响应参数

名称 Type 描述
isSuccess 布尔
errors 对象数组 错误或警告消息列表(如果有)
code 字符串 消息的错误代码
message 字符串 对错误的说明
target 字符串 发生错误的实体
responseData Object 包含请求的实际响应有效负载
pollingUrl 字符串 轮询 URL 以获取进行中的提交所处状态
ongoingSubmissionId 字符串 进行中的提交的提交 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

路径参数

名称 描述
productId 产品的合作伙伴中心 ID
PackageId 要提取包的唯一 ID

必需标头

标头
Authorization: Bearer <Token> 使用以合作伙伴中心帐户注册的 Azure AD 应用 ID
X-Seller-Account-Id 合作伙伴中心帐户的卖家 ID

响应头

标头
X-Correlation-ID 每个请求都应该使用唯一的 GUID。 可以将其与支持团队共享以用于分析问题。
Retry-After 由于速率限制,客户端在再次调用 API 之前需要等待的时间(以秒为单位)。

响应参数

名称 Type 描述
isSuccess 布尔
errors 对象数组 错误或警告消息列表(如果有)
code 字符串 消息的错误代码
message 字符串 对错误的说明
target 字符串 发生错误的实体
responseData Object
对象数组 用于保存包模块数据的对象
PackageId 字符串
packageUrl 字符串
语言 字符串数组
architectures 字符串数组 [Neutral, X86, X64, Arm, Arm64]
isSilentInstall 布尔 如果安装程序以静默模式运行,而无需切换,则应将此项标记为 true,否则为 false
installerParameters 字符串
genericDocUrl 字符串
errorDetails 对象数组
errorScenario 字符串
errorScenarioDetails 对象数组
errorValue 字符串
errorUrl 字符串
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 时,请求需要包括所有模块数据,以便完整更新每个字段。 未请求的字段默认值将覆盖该模块的当前值。 这会使请求的新包集覆盖所有现有包。 这将重新包 ID 重新生成,用户应调用最新包 ID 的 GET 包 API。

使用单包程序模块更新 API 时,只有给定包中需更新的字段才需要在请求中出现。 请求到的字段值将覆盖其现有值,其他所有未在请求中出现的字段,将与该包的当前字段相同。 集合中的其他包保持不变。

路径参数

名称 描述
productId 产品的合作伙伴中心 ID
PackageId 包的唯一 ID

必需标头

标头
Authorization: Bearer <Token> 使用以合作伙伴中心帐户注册的 Azure AD 应用 ID
X-Seller-Account-Id 合作伙伴中心帐户的卖家 ID

请求参数

名称 Type 描述
对象数组 用于保存包模块数据的对象 [仅在完整模块更新时需要]
packageUrl 字符串 必需
语言 字符串数组 必需
architectures 字符串数组 必需 应只包含一种架构 - Neutral、X86、X64、Arm、Arm64
isSilentInstall 布尔 必需 如果安装程序以静默模式运行,而无需切换,则应将此项标记为 true,否则为 false
installerParameters 字符串 如果 isSilentInstall 为 false,则为必需
genericDocUrl 字符串 当packageType 是 exe 时必需 文档链接包含 EXE 类型安装程序的自定义错误代码详情
errorDetails 对象数组 用于保存 EXE 类型安装程序的自定义错误代码和详细信息的元数据。
errorScenario 字符串 确定具体错误发生的场景。 [installationCancelled、ByUserapplicationAlreadyExistsinstallationAlreadyInProgress、diskSpaceIsFull、rebootRequired、networkFailure、packageRejectedDuringInstallation、installationSuccessful、miscellaneous]
errorScenarioDetails 对象数组
errorValue 字符串 安装过程中可能出现的错误代码
errorUrl 字符串 跳转错误详情的 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 之前需要等待的时间(以秒为单位)。

响应参数

名称 Type 描述
isSuccess 布尔
errors 对象数组 [错误或警告消息列表(如果有)]
code 字符串 消息的错误代码
message 字符串 对错误的说明
target 字符串 发生错误的实体
responseData Object
pollingUrl 字符串 [轮询 URL 以获取进行中的提交所处状态]
ongoingSubmissionId 字符串 [进行中的提交的提交 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

路径参数

名称 描述
productId 产品的合作伙伴中心 ID

必需标头

标头
Authorization: Bearer <Token> 使用以合作伙伴中心帐户注册的 Azure AD 应用 ID
X-Seller-Account-Id 合作伙伴中心帐户的卖家 ID

响应头

标头
X-Correlation-ID 每个请求都应该使用唯一的 GUID。 可以将其与支持团队共享以用于分析问题。
Retry-After 由于速率限制,客户端在再次调用 API 之前需要等待的时间(以秒为单位)。

响应参数

名称 Type 描述
isSuccess 布尔
errors 对象数组 [错误或警告消息列表(如果有)]
code 字符串 消息的错误代码
message 字符串 对错误的说明
target 字符串 发生错误的实体
responseData Object
pollingUrl 字符串 [轮询 URL 以进行中的提交的获取包上传状态或提交状态]
ongoingSubmissionId 字符串 [进行中的提交的提交 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

路径参数

名称 描述
productId 产品的合作伙伴中心 ID

查询参数

名称 描述
语言 [可选]:列表语言筛选器使用逗号分隔字符串 [最多 200 种语言]。 如果空置,将提取前 200 个可用的列表语言资产数据。 (例如“en-us, en-gb")

必需标头

标头
Authorization: Bearer <Token> 使用以合作伙伴中心帐户注册的 Azure AD 应用 ID
X-Seller-Account-Id 合作伙伴中心帐户的卖家 ID

响应头

标头
X-Correlation-ID 每个请求都应该使用唯一的 GUID。 可以将其与支持团队共享以用于分析问题。
Retry-After 由于速率限制,客户端在再次调用 API 之前需要等待的时间(以秒为单位)。

响应参数

名称 Type 描述
isSuccess 布尔
errors 对象数组 错误或警告消息列表(如果有)
code 字符串 消息的错误代码
message 字符串 对错误的说明
target 字符串 发生错误的实体
responseData Object
listingAssets 对象数组 列出每种语言的资产详细信息
language 字符串
StoreLogo 对象数组
屏幕截图 对象数组
id 字符串
assetUrl 字符串 必须是有效的 URI
imageSize Object
width Integer
height Integer

示例响应

{   
"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(跳转 Blob 存储)为每个图像上传资产,以及上传成功后的提交 API 调用。 如果要能够更新列表资产,反之,要能够在列表模块中添加/删除区域设置,可以使用以下方法:

  1. 使用创建列表资产 API 发送有关资产上传的请求以及语言、类型和资产计数。
  2. 资产 ID 会根据请求的资产数按需创建,短期 SAS URL 也将在创建后以资产类型按照正文格式回传。 可以使用此 URL 通过 HTTP 客户端上传特定类型的图像资产 [Put Blob (REST API) - Azure 存储 |Microsoft Docs]。
  3. 上传后,可以使用提交列表资产 API 发送新资产 ID 信息,这些信息来自早些时候调用过的 API。 验证后,该 API 将在验证后内部提交资产列表数据。
  4. 特定语言(已在请求中发送)的资产类型下的所有旧图片集,可由此方法有效覆盖。 因此,以前上传的资产将被删除。

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

路径参数

名称 描述
productId 产品的合作伙伴中心 ID

必需标头

Header 说明
Authorization: Bearer <Token> 使用以合作伙伴中心帐户注册的 Azure AD 应用 ID
X-Seller-Account-Id 合作伙伴中心帐户的卖家 ID

请求参数

名称 Type 说明
语言 字符串 必需
createAssetRequest Object 必需
屏幕快照 Integer 在 ISV 需要更新屏幕截图或添加新列表语言时必需[1 - 10]
徽标 Integer 在 ISV 需要更新徽标或添加新列表语言时必需[1 或 2]

响应头

标头 说明
X-Correlation-ID 每个请求都应该使用唯一的 GUID。 可以将其与支持团队共享以用于分析问题。
Retry-After 由于速率限制,客户端在再次调用 API 之前需要等待的时间(以秒为单位)。

响应参数

名称 Type 描述
isSuccess 布尔
errors 对象数组 错误或警告消息列表(如果有)
code 字符串 消息的错误代码
message 字符串 对错误的说明
target 字符串 发生错误的实体
responseData Object
listingAssets Object 包含待上传的 StoreLogos 详情和屏幕截图的对象
language 字符串
StoreLogo 对象数组
屏幕截图 对象数组
id 字符串
primaryAssetUploadUrl 字符串 使用 Azure Blob REST API 上传列表资产的主要 URL
secondaryAssetUploadUrl 字符串 使用 Azure Blob REST API 上传列表资产的次要 URL
httpMethod HTTP 方法 需要使用 HTTP 方法通过资产上传 URL 上传资产(主要或次要)
httpHeaders Object 对象的键作为资产上传 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

路径参数

名称 描述
productId 产品的合作伙伴中心 ID

必需标头

Header 说明
Authorization: Bearer <Token> 使用以合作伙伴中心帐户注册的 Azure AD 应用 ID
X-Seller-Account-Id 合作伙伴中心帐户的卖家 ID

请求参数

名称 Type 描述
listingAssets Object
language 字符串
StoreLogo 对象数组
屏幕截图 对象数组
id 字符串 必须是来自获取当前列表资产 API,且用户希望延续的现有 ID,或在使用创建列表资产API 新上传资产的新 ID。
assetUrl 字符串 必须是来自获取当前列表资产 API 用户希望延续的现有资产 URL,或使用创建列表资产 API 上传新资产的 URL(主要或次要)。 必须是有效的 URI

示例请求

{
    "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 之前需要等待的时间(以秒为单位)。

响应参数

名称 Type 描述
isSuccess 布尔
errors 对象数组 错误或警告消息列表(如果有)
code 字符串 消息的错误代码
message 字符串 对错误的说明
target 字符串 发生错误的实体
responseData Object
pollingUrl 字符串 轮询 URL 以获取进行中的提交所处状态
ongoingSubmissionId 字符串 进行中的提交的提交 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

路径参数

名称 描述
productId 产品的合作伙伴中心 ID

必需标头

Header 说明
Authorization: Bearer <Token> 使用以合作伙伴中心帐户注册的 Azure AD 应用 ID
X-Seller-Account-Id 合作伙伴中心帐户的卖家 ID

响应头

标头 说明
X-Correlation-ID 每个请求都应该使用唯一的 GUID。 可以将其与支持团队共享以用于分析问题。
Retry-After 由于速率限制,客户端在再次调用 API 之前需要等待的时间(以秒为单位)。

响应参数

名称 Type 描述
isSuccess 布尔
errors 对象数组 错误或警告消息列表(如果有)
code 字符串 消息的错误代码
message 字符串 对错误的说明
target 字符串 发生错误的实体
responseData Object
isReady 布尔 表明是否所有模块已处于就绪状态,包括包上传
ongoingSubmissionId 字符串 进行中的提交的提交 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 将检查:

  • 活动提交,如果存在活动提交,则失败并显示错误消息。
  • 是否所有模块都处于可创建提交的就绪状态。
  • 是否已根据应用商店的要求对提交中的每个字段进行验证

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

路径参数

名称 描述
productId 产品的合作伙伴中心 ID

必需标头

Header 说明
Authorization: Bearer <Token> 使用以合作伙伴中心帐户注册的 Azure AD 应用 ID
X-Seller-Account-Id 合作伙伴中心帐户的卖家 ID

响应头

标头 说明
X-Correlation-ID 每个请求都应该使用唯一的 GUID。 可以将其与支持团队共享以用于分析问题。
Retry-After 由于速率限制,客户端在再次调用 API 之前需要等待的时间(以秒为单位)。

响应参数

名称 Type 描述
isSuccess 布尔
errors 对象数组 错误或警告消息列表(如果有)
code 字符串 消息的错误代码
message 字符串 对错误的说明
target 字符串 发生错误的实体
responseData Object
pollingUrl 字符串 轮询 URL 以获取模块准备状态,包括提交包上传
submissionId 字符串 新建提交的 ID
ongoingSubmissionId 字符串 进行中的提交的提交 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

路径参数

名称 描述
productId 产品的合作伙伴中心 ID

必需标头

Header 说明
Authorization: Bearer <Token> 使用以合作伙伴中心帐户注册的 Azure AD 应用 ID
X-Seller-Account-Id 合作伙伴中心帐户的卖家 ID

响应头

标头 说明
X-Correlation-ID 每个请求都应该使用唯一的 GUID。 可以将其与支持团队共享以用于分析问题。
Retry-After 由于速率限制,客户端在再次调用 API 之前需要等待的时间(以秒为单位)。

响应参数

名称 Type 描述
isSuccess 布尔
errors 对象数组 错误或警告消息列表(如果有)
code 字符串 消息的错误代码
message 字符串 对错误的说明
target 字符串 发生错误的实体
responseData Object
publishingStatus 字符串 提交发布状态 - [INPROGRESS, PUBLISHED, FAILED, UNKNOWN]
hasFailed 布尔 说明发布是否失败且不会重试

示例响应

{
    "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 属性分配给合作伙伴中心帐户的卖家 ID。
  • 将 ApplicationId 属性分配给需要管理的应用ID。
  • 将 ClientId 和 ClientSecret 属性分配给应用的客户端 ID 和密钥,并将 TokenEndpoint URL 中的 tenantid 字符串替换为应用的租户 ID。 有关详细信息,请参阅 如何将 Azure AD 应用程序与你的合作伙伴中心帐户相关联
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 帮助程序类将 Azure Active Directory 数据和应用数据传递给使用 Microsoft Store 提交 API 的每个示例方法。

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

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

        /// <summary>
        /// Client secret of your Azure Active 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 Azure Active 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 方法的 Azure AD 访问令牌。 获取令牌后,可以在 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 Azure Active 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
        /// Azure Active Directory app. Example: https://login.microsoftonline.com/d454d300-128e-2d81-334a-27d9b2baf002/oauth2/v2.0/token </param>
        /// <param name="clientId">Client Id of your Azure Active Directory app. Example" 00001111-aaaa-2222-bbbb-3333cccc4444</param>
        /// <param name="clientSecret">Client secret of your Azure Active 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 install node-fetch@2]

使用 node.js 创建应用提交

以下示例调用本文中的其他示例方法,以演示使用 Microsoft Store 提交 API 的不同方法。 要调整此代码以供自己使用,请执行以下操作:

  • 将 SellerId 属性分配给合作伙伴中心帐户的卖家 ID。
  • 将 ApplicationId 属性分配给需要管理的应用ID。
  • 将 ClientId 和 ClientSecret 属性分配给应用的客户端 ID 和密钥,并将 TokenEndpoint URL 中的 tenantid 字符串替换为应用的租户 ID。 有关详细信息,请参阅 如何将 Azure AD 应用程序与你的合作伙伴中心帐户相关联

以下示例使用的类调用了 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 帮助程序类将 Azure Active Directory 数据和应用数据传递给使用 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 方法的 Azure AD 访问令牌。 获取令牌后,可以在 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 来管理提交的帮助,请访问支持页面并请求帮助:

  • 在我们的论坛中提问。
  • 请访问我们的支持页面,请求合作伙伴中心的辅助支持选项。 如果系统提示选择问题类型和类别,请分别选择“应用提交和认证”和“提交应用”。