适用于 MSI 或 EXE 应用的 Microsoft Store 提交 API
使用 适用于 MSI 或 EXE 应用的 Microsoft Store 提交 API 以编程方式查询,并为你的或组织的合作伙伴中心帐户的应用、加载项和软件包外部测试版创建提交。 当你的帐户管理着多个应用或加载项,而你想要自动执行并优化这些资源的提交过程时,此 API 十分实用。 此 API 使用 Azure Active Directory (Azure AD) 验证来自应用或服务的调用。
以下步骤介绍了使用 Microsoft Store 提交 API 的端到端过程:
- 确保已完成所有先决条件。
- 在调用 Microsoft Store 提交 API 中的方法之前,请先获取 Azure AD 访问令牌。 获取令牌后,可以在 60 分钟的令牌有效期内,使用该令牌调用“Microsoft Store 提交 API”。 该令牌到期后,可以重新生成一个。
- 调用适用于 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 的准备:
- 如果合作伙伴中心中尚不存在你的应用,需要通过在合作伙伴中心保留其名称来创建你的应用。 不能使用 Microsoft Store 提交 API 在合作伙伴中心创建应用;需要在合作伙伴中心进行创建,然后才能使用 API 访问应用,并以编程方式为其创建提交。
- 在可以使用此 API 为给定应用创建提交之前,首先必须在合作伙伴中心为应用创建一个提交,其中包括回答年龄分级调查表的内容。 执行此操作后,就能够使用 API 以编程方式为此应用创建新提交。
- 在创建或更新应用提交时,如果需要提交新包,请准备程序包详细信息。
- 在创建或更新应用提交时,如果需要同时提交 Store 一览的屏幕截图或图像,请准备应用屏幕截图和图像。
如何将 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 访问令牌时,都可以重复使用它们。
- 在合作伙伴中心,将组织的合作伙伴中心帐户与组织的 Azure AD 目录相关联。
- 接下来,在合作伙伴中心的用户页的帐户设置部分添加 Azure AD 应用程序,即你要以合作伙伴中心帐户访问提交的应用或服务。 确保为此应用程序分配“管理者”角色。 如果你的 Azure AD 目录中尚不包含该应用程序,可以在合作伙伴中心创建新的 Azure AD 应用程序。
- 返回到“用户”页,单击 Azure AD 应用程序的名称转到“应用程序设置”,然后复制“租户 ID”和“客户端 ID”值。
- 要添加新密钥或客户端密码,请参阅以下说明,或参考通过 Azure 门户注册应用的说明:
要注册应用,请执行以下操作:
登录到 Azure 门户。
如果有权访问多个租户,请使用顶部菜单中的“目录 + 订阅”筛选器 ,以切换到要在其中注册应用程序的租户。
搜索并选择“Azure Active Directory”。
在“管理”下,选择“应用注册”>,然后选择你的应用程序 。
选择“证书和机密”>“客户端密码”>“新建客户端密码”。
添加客户端机密的说明。
选择机密的过期时间,或指定自定义的生存期。
客户端机密生存期限制为两年(24 个月)或更短。 不能指定超过 24 个月的自定义生存期。
注意
Microsoft 建议将过期时间值设置为小于 12 个月。
选择 添加 。
记录机密的值,以便在客户端应用程序代码中使用。 退出此页面后,此机密值永不再显示。
步骤 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_id
和 client_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 调用。 如果要能够更新列表资产,反之,要能够在列表模块中添加/删除区域设置,可以使用以下方法:
- 使用创建列表资产 API 发送有关资产上传的请求以及语言、类型和资产计数。
- 资产 ID 会根据请求的资产数按需创建,短期 SAS URL 也将在创建后以资产类型按照正文格式回传。 可以使用此 URL 通过 HTTP 客户端上传特定类型的图像资产 [Put Blob (REST API) - Azure 存储 |Microsoft Docs]。
- 上传后,可以使用提交列表资产 API 发送新资产 ID 信息,这些信息来自早些时候调用过的 API。 验证后,该 API 将在验证后内部提交资产列表数据。
- 特定语言(已在请求中发送)的资产类型下的所有旧图片集,可由此方法有效覆盖。 因此,以前上传的资产将被删除。
路径:/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 来管理提交的帮助,请访问支持页面并请求帮助: