简介
零触摸注册 API 可帮助设备转销商自动进行集成。贵组织的销售工具可用于构建零触摸注册,让您的用户和客户的工作效率更高。使用 API 帮助您的用户:
- 将购买的设备分配给客户的零触摸注册帐号。
- 为您的客户创建零触摸注册帐号。
- 将贵组织的电话和订单元数据附加到设备。
- 针对分配给您的客户的设备创建报告。
本文档介绍了此 API 并介绍了相关模式。如果您想自行探索该 API,请尝试阅读 Java、.NET 或 Python 快速入门。
API 概念
客户和设备是您在 API 中使用的核心资源。如需创建客户,请调用 create
。您可以使用声明 API 方法创建设备(见下文)。您的组织也可以使用零触摸注册门户创建客户和设备。
- 客户
- 贵组织向其销售设备的公司。客户有
name
和ID
。如要申领或查找客户的设备,请询问客户。如需了解详情,请参阅Customer
。 - 设备
- 贵组织向客户销售的支持零触摸注册的 Android 或 ChromeOS 设备。设备具有硬件 ID、元数据和客户声明。设备是 API 的核心,因此您在几乎所有方法中都会用到设备。如需了解详情,请参阅
Device
。 - DeviceIdentifier
- 封装用于识别制造设备的硬件 ID,例如 IMEI 或 MEID。使用
DeviceIdentifier
定位到您要查找、更新或声明的设备。如需了解详情,请参阅标识符。 - DeviceMetadata
- 存储设备的元数据键值对。使用
DeviceMetadata
存储组织的元数据。如需了解详情,请参阅设备元数据。
如需列出您的应用可以使用的所有 API 方法和资源,请参阅 API 参考文档。
创建客户
对于 Android 设备,转销商负责代表客户创建客户帐号。客户将使用此帐号访问零触摸门户,为其设备配置配置设置。对于已经拥有 Google Workspace 帐号用于配置配置设置的 ChromeOS 设备,则不需要执行此操作。
您可以调用 create
API 方法来创建用于零触摸注册的客户帐号。由于客户会在其零触摸注册门户中看到公司名称,因此应用用户应确认其正确无误。创建客户后,您便无法修改客户的名称。
您需要至少添加一个与 Google 帐号相关联的公司电子邮件地址,才能成为所有者。您无法将个人 Gmail 帐号与 API 搭配使用。如果客户在关联账号方面需要帮助,请发送关联 Google 账号中的说明。
在您通过调用 API 创建客户后,客户会管理其员工的门户访问权限,而您无法使用 API 修改客户的用户。以下代码段展示了如何创建客户:
Java
// Provide the customer data as a Company type. // The API requires a name and owners. Company customer = new Company(); customer.setCompanyName("XYZ Corp"); customer.setOwnerEmails(Arrays.asList("liz@example.com", "darcy@example.com")); customer.setAdminEmails(Collections.singletonList("jane@example.com")); // Use our reseller ID for the parent resource name. String parentResource = String.format("partners/%d", PARTNER_ID); // Call the API to create the customer using the values in the company object. CreateCustomerRequest body = new CreateCustomerRequest(); body.setCustomer(customer); Company response = service.partners().customers().create(parentResource, body).execute();
.NET
// Provide the customer data as a Company type. // The API requires a name and owners. var customer = new Company { CompanyName = "XYZ Corp", OwnerEmails = new String[] { "liz@example.com", "darcy@example.com" }, AdminEmails = new String[] { "jane@example.com" } }; // Use our reseller ID for the parent resource name. var parentResource = String.Format("partners/{0}", PartnerId); // Call the API to create the customer using the values in the company object. var body = new CreateCustomerRequest { Customer = customer }; var request = service.Partners.Customers.Create(body, parentResource); var response = request.Execute();
Python
# Provide the customer data as a Company type. The API requires # a name and at least one owner. company = {'companyName':'XYZ Corp', \ 'ownerEmails':['liz@example.com', 'darcy@example.com'], \ 'adminEmails':['jane@example.com']} # Use our reseller ID for the parent resource name. parent_resource = 'partners/{0}'.format(PARTNER_ID) # Call the API to create the customer using the values in the company object. response = service.partners().customers().create(parent=parent_resource, body={'customer':company}).execute()
如需详细了解客户员工的所有者和管理员角色,请参阅门户用户。
为客户领取设备
客户购买设备后,需要在帐号中为这些设备配置配置设置。认领设备会将设备添加到零触摸注册,并使客户能够配置配置设置。
设备的配置记录包含用于零触摸注册的部分。您可以通过为客户声明该记录的零触摸注册部分来分配设备。使用客户作为参数,调用 partners.devices.claim
或 partners.devices.claimAsync
方法。始终提供 SECTION_TYPE_ZERO_TOUCH
作为 sectionType
的值。
您需要先取消声明(见下文)某个客户的设备,然后才能为其他客户声明同一设备。在创建新设备时,声明方法会validate DeviceIdentifier
字段,包括 IMEI 或 MEID,或序列号、制造商名称和型号,以及 ChromeOS 设备的经认证设备 ID。
以下代码段展示了如何认领设备:
Java
// Identify the device to claim. DeviceIdentifier identifier = new DeviceIdentifier(); // The manufacturer value is optional but recommended for cellular devices identifier.setManufacturer("Google"); identifier.setImei("098765432109875"); // Create the body to connect the customer with the device. ClaimDeviceRequest body = new ClaimDeviceRequest(); body.setDeviceIdentifier(identifier); body.setCustomerId(customerId); body.setSectionType("SECTION_TYPE_ZERO_TOUCH"); // Claim the device. ClaimDeviceResponse response = service.partners().devices().claim(PARTNER_ID, body).execute();
.NET
// Identify the device to claim. var deviceIdentifier = new DeviceIdentifier { // The manufacturer value is optional but recommended for cellular devices Manufacturer = "Google", Imei = "098765432109875" }; // Create the body to connect the customer with the device. ClaimDeviceRequest body = new ClaimDeviceRequest { DeviceIdentifier = deviceIdentifier, CustomerId = CustomerId, SectionType = "SECTION_TYPE_ZERO_TOUCH" }; // Claim the device. var response = service.Partners.Devices.Claim(body, PartnerId).Execute();
Python
# Identify the device to claim. # The manufacturer value is optional but recommended for cellular devices device_identifier = {'manufacturer':'Google', 'imei':'098765432109875'} # Create the body to connect the customer with the device. request_body = {'deviceIdentifier':device_identifier, \ 'customerId':customer_id, \ 'sectionType':'SECTION_TYPE_ZERO_TOUCH'} # Claim the device. response = service.partners().devices().claim(partnerId=PARTNER_ID, body=request_body).execute()
正在取消声明设备
贵组织可以取消客户对设备的所有权。取消声明设备会将其从零触摸注册中移除。转销商可能会取消对要迁移到其他帐号、被退回或误认领的设备的所有权。调用 partners.devices.unclaim
或 partners.devices.unclaimAsync
方法,向客户取消声明对设备的所有权。
供应商
您可以使用供应商来代表经销商网络中的转销商合作伙伴、全球转销商网络中的本地运营商,或者代表您销售设备的任何组织。供应商可帮助您分离用户、客户和设备:
- 您创建的供应商无法查看您的零触摸注册帐号或彼此的帐号。
- 您可以查看供应商的客户和设备,也可以取消注册供应商的设备。但是,您无法将设备分配给供应商的客户。
请使用门户为组织创建供应商 - 您无法使用 API。您的帐号角色必须是 Owner 才能创建新供应商。如果贵组织有供应商,您可以调用 partners.vendors.list
列出供应商,调用 partners.vendors.customers.list
获取供应商的客户。以下示例使用这两种方法输出一份报告,其中显示供应商客户的服务条款状态:
Java
// First, get the organization's vendors. String parentResource = String.format("partners/%d", PARTNER_ID); ListVendorsResponse results = service.partners().vendors().list(parentResource).execute(); if (results.getVendors() == null) { return; } // For each vendor, report the company name and a maximum 5 customers. for (Company vendor: results.getVendors()) { System.out.format("\n%s customers\n", vendor.getCompanyName()); System.out.println("---"); // Use the vendor's API resource name as the parent resource. AndroidProvisioningPartner.Partners.Vendors.Customers.List customerRequest = service.partners().vendors().customers().list(vendor.getName()); customerRequest.setPageSize(5); ListVendorCustomersResponse customerResponse = customerRequest.execute(); List<Company> customers = customerResponse.getCustomers(); if (customers == null) { System.out.println("No customers"); break; } else { for (Company customer: customers) { System.out.format("%s: %s\n", customer.getCompanyName(), customer.getTermsStatus()); } } }
.NET
// First, get the organization's vendors. var parentResource = String.Format("partners/{0}", PartnerId); var results = service.Partners.Vendors.List(parentResource).Execute(); if (results.Vendors == null) { return; } // For each vendor, report the company name and a maximum 5 customers. foreach (Company vendor in results.Vendors) { Console.WriteLine("\n{0} customers", vendor); Console.WriteLine("---"); // Use the vendor's API resource name as the parent resource. PartnersResource.VendorsResource.CustomersResource.ListRequest customerRequest = service.Partners.Vendors.Customers.List(vendor.Name); customerRequest.PageSize = 5; var customerResponse = customerRequest.Execute(); IList<Company> customers = customerResponse.Customers; if (customers == null) { Console.WriteLine("No customers"); break; } else { foreach (Company customer in customers) { Console.WriteLine("{0}: {1}", customer.Name, customer.TermsStatus); } } }
Python
# First, get the organization's vendors. parent_resource = 'partners/{0}'.format(PARTNER_ID) vendor_response = service.partners().vendors().list( parent=parent_resource).execute() if 'vendors' not in vendor_response: return # For each vendor, report the company name and a maximum 5 customers. for vendor in vendor_response['vendors']: print '\n{0} customers'.format(vendor['companyName']) print '---' # Use the vendor's API resource name as the parent resource. customer_response = service.partners().vendors().customers().list( parent=vendor['name'], pageSize=5).execute() if 'customers' not in customer_response: print 'No customers' break for customer in customer_response['customers']: print ' {0}: {1}'.format(customer['name'], customer['termsStatus'])
如果您拥有一系列设备,则可能需要知道哪个转销商或供应商声明了该设备。如需获取数字形式的转销商 ID,请检查设备声明记录中 resellerId
字段的值。
贵组织可以取消对供应商已声明的设备的所有权。对于修改设备的其他 API 调用,您应该先检查您的组织是否声明了设备的所有权,然后再调用相应 API 方法。以下示例展示了如何执行此操作:
Java
// Get the devices claimed for two customers: one of our organization's // customers and one of our vendor's customers. FindDevicesByOwnerRequest body = new FindDevicesByOwnerRequest(); body.setSectionType("SECTION_TYPE_ZERO_TOUCH"); body.setCustomerId(Arrays.asList(resellerCustomerId, vendorCustomerId)); body.setLimit(MAX_PAGE_SIZE); FindDevicesByOwnerResponse response = service.partners().devices().findByOwner(PARTNER_ID, body).execute(); if (response.getDevices() == null) { return; } for (Device device: response.getDevices()) { // Confirm the device was claimed by our reseller and not a vendor before // updating metadata in another method. for (DeviceClaim claim: device.getClaims()) { if (claim.getResellerId() == PARTNER_ID) { updateDeviceMetadata(device.getDeviceId()); break; } } }
.NET
// Get the devices claimed for two customers: one of our organization's // customers and one of our vendor's customers. FindDevicesByOwnerRequest body = new FindDevicesByOwnerRequest { Limit = MaxPageSize, SectionType = "SECTION_TYPE_ZERO_TOUCH", CustomerId = new List<long?> { resellerCustomerId, vendorCustomerId } }; var response = service.Partners.Devices.FindByOwner(body, PartnerId).Execute(); if (response.Devices == null) { return; } foreach (Device device in response.Devices) { // Confirm the device was claimed by our reseller and not a vendor before // updating metadata in another method. foreach (DeviceClaim claim in device.Claims) { if (claim.ResellerId == PartnerId) { UpdateDeviceMetadata(device.DeviceId); break; } } }
Python
# Get the devices claimed for two customers: one of our organization's # customers and one of our vendor's customers. request_body = {'limit':MAX_PAGE_SIZE, \ 'pageToken':None, \ 'customerId':[reseller_customer_id, vendor_customer_id], \ 'sectionType':'SECTION_TYPE_ZERO_TOUCH'} response = service.partners().devices().findByOwner(partnerId=PARTNER_ID, body=request_body).execute() for device in response['devices']: # Confirm the device was claimed by our reseller and not a vendor before # updating metadata in another method. for claim in device['claims']: if claim['resellerId'] == PARTNER_ID: update_device_metadata(device['deviceId']) break
长时间运行的批量操作
该 API 包含设备方法的异步版本。这些方法允许对许多设备进行批处理,而同步方法可以为每个 API 请求处理一个设备。异步方法名称具有 Async 后缀,例如 claimAsync
。
异步 API 方法会在处理完成之前返回结果。异步方法还有助于您的应用(或工具)在用户等待长时间运行的操作完成时保持响应。您的应用应定期检查操作的状态。
运维
您可以使用 Operation
跟踪长时间运行的批量操作。如果对异步方法的成功调用,将在响应中返回对操作的引用。以下 JSON 代码段显示了调用 updateMetadataAsync
后的典型响应:
{
"name": "operations/apibatchoperation/1234567890123476789"
}
每项操作都包含单个任务的列表。调用 operations.get
以查找有关操作中包含的任务的状态和结果的信息。以下代码段展示了如何执行此操作。在您自己的应用中,您需要处理所有错误。
Java
// Build out the request body to apply the same order number to a customer's // purchase of 2 devices. UpdateMetadataArguments firstUpdate = new UpdateMetadataArguments(); firstUpdate.setDeviceMetadata(metadata); firstUpdate.setDeviceId(firstTargetDeviceId); UpdateMetadataArguments secondUpdate = new UpdateMetadataArguments(); secondUpdate.setDeviceMetadata(metadata); secondUpdate.setDeviceId(firstTargetDeviceId); // Start the device metadata update. UpdateDeviceMetadataInBatchRequest body = new UpdateDeviceMetadataInBatchRequest(); body.setUpdates(Arrays.asList(firstUpdate, secondUpdate)); Operation response = service .partners() .devices() .updateMetadataAsync(PARTNER_ID, body) .execute(); // Assume the metadata update started, so get the Operation for the update. Operation operation = service.operations().get(response.getName()).execute();
.NET
// Build out the request body to apply the same order number to a customer's // purchase of 2 devices. var updates = new List<UpdateMetadataArguments> { new UpdateMetadataArguments { DeviceMetadata = metadata, DeviceId = firstTargetDeviceId }, new UpdateMetadataArguments { DeviceMetadata = metadata, DeviceId = secondTargetDeviceId } }; // Start the device metadata update. UpdateDeviceMetadataInBatchRequest body = new UpdateDeviceMetadataInBatchRequest { Updates = updates }; var response = service.Partners.Devices.UpdateMetadataAsync(body, PartnerId).Execute(); // Assume the metadata update started, so get the Operation for the update. Operation operation = service.Operations.Get(response.Name).Execute();
Python
# Build out the request body to apply the same order number to a customer's # purchase of 2 devices. updates = [{'deviceMetadata':metadata,'deviceId':first_target_device_id}, {'deviceMetadata':metadata,'deviceId':second_target_device_id}] # Start the device metadata update. response = service.partners().devices().updateMetadataAsync( partnerId=PARTNER_ID, body={'updates':updates}).execute() # Assume the metadata update started, so get the Operation for the update. operation = service.operations().get(name=response['name']).execute()
如需了解操作是否已完成,请检查操作中是否存在值为 true
的 done
字段。如果 done
缺失或 false
,则操作仍在运行。
响应
操作完成后,API 会使用结果更新操作,即使所有任务或全部任务均未成功也是如此。response
字段是一个 DevicesLongRunningOperationResponse
对象,其中详细说明了操作中每个设备的处理情况。
检查 successCount
字段可以高效地找出是否有任务失败,并避免遍历大型结果列表。DevicesLongRunningOperationResponse
的 perDeviceStatus
字段是一个 OperationPerDevice
实例列表,其中详细说明了操作中的每个设备。列表顺序与原始请求中的任务一致。
每个 OperationPerDevice
任务都包含一个 result
字段以及服务器收到的请求的提醒摘要。使用 result
字段检查任务是成功还是失败。
以下 JSON 代码段显示了调用 updateMetadataAsync
后某个操作的典型响应的一部分:
"response": {
"perDeviceStatus": [
{
"result": {
"deviceId": "12345678901234567",
"status": "SINGLE_DEVICE_STATUS_SUCCESS"
},
"updateMetadata": {
"deviceId": "12345678901234567",
"deviceMetadata": {
"entries": {
"phonenumber": "+1 (800) 555-0100"
}
}
}
}
],
"successCount": 1
}
跟踪进度
如果您的应用需要跟踪进度,您应该定期重新获取操作。metadata
字段包含一个 DevicesLongRunningOperationMetadata
实例,可帮助应用检查正在运行的操作的最新进度。请使用下表中列出的 DevicesLongRunningOperationMetadata
字段跟踪操作的进度:
字段 | 典型用法 |
---|---|
processingStatus
|
随着操作的进行,从 BATCH_PROCESS_PENDING 更改为 BATCH_PROCESS_IN_PROGRESS ,然后更改为 BATCH_PROCESS_PROCESSED 。 |
progress
|
已处理的更新所占的百分比。您的应用可以使用此数据来估算完成时间。由于操作完成时 progress 值可能为 100,因此请检查操作的 done 字段以了解操作是否已完成并有结果。 |
devicesCount
|
显示操作中的更新次数。如果 API 无法解析某些更新,此值可能与请求中的更新次数不同。 |
下面的简化示例展示了应用如何使用进度元数据来设置轮询间隔。在您的应用中,您可能需要使用更复杂的任务运行程序来进行轮询。您还需要添加错误处理。
Java
// Milliseconds between polling the API. private static long MIN_INTERVAL = 2000; private static long MAX_INTERVAL = 10000; // ... // Start the device metadata update. Operation response = service .partners() .devices() .updateMetadataAsync(PARTNER_ID, body) .execute(); String operationName = response.getName(); // Start polling for completion. long startTime = new Date().getTime(); while (true) { // Get the latest update on the operation's progress using the API. Operation operation = service.operations().get(operationName).execute(); if (operation.get("done") != null && operation.getDone()) { // The operation is finished. Print the status. System.out.format("Operation complete: %s of %s successful device updates\n", operation.getResponse().get("successCount"), operation.getMetadata().get("devicesCount")); break; } else { // Estimate how long the operation *should* take - within min and max value. BigDecimal opProgress = (BigDecimal) operation.getMetadata().get("progress"); double progress = opProgress.longValue(); long interval = MAX_INTERVAL; if (progress > 0) { interval = (long) ((new Date().getTime() - startTime) * ((100.0 - progress) / progress)); } interval = Math.max(MIN_INTERVAL, Math.min(interval, MAX_INTERVAL)); // Sleep until the operation should be complete. Thread.sleep(interval); } }
.NET
// Milliseconds between polling the API. private static double MinInterval = 2000; private static double MaxInterval = 10000; // ... // Start the device metadata update. var response = service.Partners.Devices.UpdateMetadataAsync(body, PartnerId).Execute(); var operationName = response.Name; // Start polling for completion. var startTime = DateTime.Now; while (true) { // Get the latest update on the operation's progress using the API. Operation operation = service.Operations.Get(operationName).Execute(); if (operation.Done == true) { // The operation is finished. Print the status. Console.WriteLine("Operation complete: {0} of {1} successful device updates", operation.Response["successCount"], operation.Metadata["devicesCount"]); break; } else { // Estimate how long the operation *should* take - within min and max value. double progress = (double)(long)operation.Metadata["progress"]; double interval = MaxInterval; if (progress > 0) { interval = DateTime.Now.Subtract(startTime).TotalMilliseconds * ((100.0 - progress) / progress); } interval = Math.Max(MinInterval, Math.Min(interval, MaxInterval)); // Sleep until the operation should be complete. System.Threading.Thread.Sleep((int)interval); } }
Python
# Seconds between polling the API. MIN_INTERVAL = 2; MAX_INTERVAL = 10; # ... # Start the device metadata update response = service.partners().devices().updateMetadataAsync( partnerId=PARTNER_ID, body={'updates':updates}).execute() op_name = response['name'] start_time = time.time() # Start polling for completion while True: # Get the latest update on the operation's progress using the API op = service.operations().get(name=op_name).execute() if 'done' in op and op['done']: # The operation is finished. Print the status. print('Operation complete: {0} of {1} successful device updates'.format( op['response']['successCount'], op['metadata']['devicesCount'] )) break else: # Estimate how long the operation *should* take - within min and max. progress = op['metadata']['progress'] interval = MIN_INTERVAL if progress > 0: interval = (time.time() - start_time) * ((100.0 - progress) / progress) interval = max(MIN_INTERVAL, min(interval, MAX_INTERVAL)) # Sleep until the operation should be complete. time.sleep(interval)
选择对应用用户有意义的轮询方法。某些应用在等待某个流程完成时可能会受益于定期进度更新。
分页结果
partners.devices.findByOwner
API 方法可能会返回非常大的设备列表。为了减小响应大小,此方法和其他 API 方法(如 partners.devices.findByIdentifier
)支持分页结果。通过分页结果,您的应用可以迭代地请求和处理大型列表,一次一页。
调用 API 方法后,检查响应是否包含 nextPageToken
的值。如果 nextPageToken
不为 null
,您的应用可以通过再次调用该方法,使用它来提取设备的另一页。您需要在 limit
参数中设置设备数量上限。如果 nextPageToken
为 null
,则表示您的应用请求了最后一个页面。
以下示例方法展示了您的应用如何才能逐页输出设备列表:
Java
private static long MAX_PAGE_SIZE = 10; // ... /** * Demonstrates how to loop through paginated lists of devices. * @param pageToken The token specifying which result page to return. * @throws IOException If the zero-touch API call fails. */ private void printDevices(String pageToken) throws IOException { // Create the request body to find the customer's devices. FindDevicesByOwnerRequest body = new FindDevicesByOwnerRequest(); body.setLimit(MAX_PAGE_SIZE); body.setSectionType("SECTION_TYPE_ZERO_TOUCH"); body.setCustomerId(Collections.singletonList(targetCustomerId)); // Call the API to get a page of Devices. Send a page token from the method // argument (might be None). If the page token is None, the API returns the first page. FindDevicesByOwnerResponse response = service.partners().devices().findByOwner(PARTNER_ID, body).execute(); if (response.getDevices() == null) { return; } // Print the devices included in this page of results. for (Device device: response.getDevices()) { System.out.format("Device %s\n", device.getName()); } System.out.println("---"); // Check to see if another page of devices is available. If yes, // fetch and print the devices. if (response.getNextPageToken() != null) { this.printDevices(response.getNextPageToken()); } } // ... // Pass null to start printing the first page of devices. printDevices(null);
.NET
private static int MaxPageSize = 10; // ... /// <summary>Demonstrates how to loop through paginated lists of devices.</summary> /// <param name="pageToken">The token specifying which result page to return.</param> private void PrintDevices(string pageToken) { // Create the request body to find the customer's devices. FindDevicesByOwnerRequest body = new FindDevicesByOwnerRequest { PageToken = pageToken, Limit = MaxPageSize, SectionType = "SECTION_TYPE_ZERO_TOUCH", CustomerId = new List<long?> { targetCustomerId } }; // Call the API to get a page of Devices. Send a page token from the method // argument (might be None). If the page token is None, the API returns the first page. var response = service.Partners.Devices.FindByOwner(body, PartnerId).Execute(); if (response.Devices == null) { return; } // Print the devices included in this page of results. foreach (Device device in response.Devices) { Console.WriteLine("Device: {0}", device.Name); } Console.WriteLine("---"); // Check to see if another page of devices is available. If yes, // fetch and print the devices. if (response.NextPageToken != null) { this.PrintDevices(response.NextPageToken); } } // ... // Pass null to start printing the first page of devices. PrintDevices(null);
Python
MAX_PAGE_SIZE = 10; # ... def print_devices(page_token): """Demonstrates how to loop through paginated lists of devices. Args: page_token: The token specifying which result page to return. """ # Create the body to find the customer's devices. request_body = {'limit':MAX_PAGE_SIZE, \ 'pageToken':page_token, \ 'customerId':[target_customer_id], \ 'sectionType':'SECTION_TYPE_ZERO_TOUCH'} # Call the API to get a page of Devices. Send a page token from the method # argument (might be None). If the page token is None, # the API returns the first page. response = service.partners().devices().findByOwner(partnerId=PARTNER_ID, body=request_body).execute() # Print the devices included in this page of results. for device in response['devices']: print 'Device: {0}'.format(device['name']) print '---' # Check to see if another page of devices is available. If yes, # fetch and print the devices. if 'nextPageToken' in response: print_devices(response['nextPageToken']) # ... # Pass None to start printing the first page of devices. print_devices(None);