Part 1: Apple Wallet Passbook RESTful Web Services Using ASP.Net

Yang Zhou
4 min readDec 12, 2018

--

PassKit Web Service Reference suggests 5 web services should be implemented to allow Apple Wallet communicating with your server, such as updating or deleting Apple Passes. This article will show you how to implement these web services in ASP.NET.

You’re the server end, and Wallet is the client. Below graph roughly shows when these web services are called. I suggest implementing the “Log” web service first. It will show you error information responds from Wallet.

Web services for Apple Pass

Web API Controllers

I put 5 web services into 3 controllers: DevicesController, PassesController, and LogController.

Device Controller

public class DevicesController : ApiController{
// GET request to webServiceURL/version/devices/deviceLibraryIdentifier/registrations/passTypeIdentifier
[HttpGet]
public HttpResponseMessage GetSerialNumber(string deviceLibraryIdentifier, string passTypeIdentifier)
{
}
// GET request to webServiceURL/version/devices/deviceLibraryIdentifier/registrations/passTypeIdentifier?passesUpdatedSince=tag
[HttpGet]
public HttpResponseMessage GetSerialNumber(string deviceLibraryIdentifier, string passTypeIdentifier, string passesUpdatedSince)
{
// For example...
SerialNumbers lastUpdateToSerialNumDict = new SerialNumbers();
// LastUpdated timestamp set to current datetime
lastUpdateToSerialNumDict.lastUpdated = String.Format("{0:MM/dd/yyyy HH:mm:ss}", DateTime.Now);
// A list of serial numbers got from database
lastUpdateToSerialNumDict.serialNumbers = serialNumList;
string jsonRes = JsonConvert.SerializeObject(lastUpdateToSerialNumDict);
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK);
response.Content = new StringContent(jsonRes, Encoding.UTF8, "application/json");
return response;
}
// POST request to webServiceURL/version/devices/deviceLibraryIdentifier/registrations/passTypeIdentifier/serialNumber
[HttpPost]
public HttpResponseMessage RegisterDevice(string deviceLibraryIdentifier, string passTypeIdentifier, string serialNumber, [FromBody]ApplePassServiceData.DevicesPayload payload)
{
//Save to database, udpate Devices, Passes, Register table
}
// DELETE request to webServiceURL/version/devices/deviceLibraryIdentifier/registrations/passTypeIdentifier/serialNumber
[HttpDelete]
public HttpResponseMessage UnRegisterDevice(string deviceLibraryIdentifier, string passTypeIdentifier, string serialNumber)
{
//Udpate Devices and Register table
}
}

Logs Controller

POST request to webServiceURL/version/log. The POST payload is a JSON dictionary, containing a single key and value: logs (string) — An array of log messages as strings.

public class LogController : ApiController
{
public HttpResponseMessage Post([FromBody]ApplePassServiceData.LogPayload payload)
{
string logStr = String.Join("; ", payload.logs;
// Save to ApplePassAPILog SQL table
}
}

Passes Controller

This controller implements a GET method to send latest apple pass (.pkpass) to Wallet. The response requires last-modified in the header.

public class PassesController : ApiController{
public HttpResponseMessage Get(string passTypeIdentifier, string serialNumber)
{
// If there was new updates, re-generate the whole Pass.
byte[] passBytes= [YourMethodToGeneratePass]([YourParameter]);
// Return the reponse
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK);
var dataStream = new MemoryStream(passBytes);
response.Content = new StreamContent(dataStream);
response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/vnd.apple.pkpass");
response.Content.Headers.LastModified = DateTime.Now;
return response;
}}

A few classes mentioned above to map complex body payload:

public class ApplePassServiceData
{

/*
* Apple Wallet post payload is a JSON dictionary containing a single key and value: pushToken
**/
public class DevicesPayload
{
public string pushToken { get; set; }
}
/*
* If there are matching passes, returns HTTP status 200 with a JSON dictionary with the following keys and values
*/
public class SerialNumbers
{
public string lastUpdated { get; set; }
public List<string> serialNumbers { get; set; }
}
public class LogPayload
{
public string[] logs { get; set; }
}
}

Routes(in Global.asax)

void Application_Start(object sender, EventArgs e)
{
//Other stuff ...
RouteTable.Routes.MapHttpRoute(
name: "ApplePassDeviceApi",
routeTemplate: "{version}/{controller}/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber}"
RouteTable.Routes.MapHttpRoute(
name: "ApplePassDeviceApi2",
routeTemplate: "{version}/{controller}/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}"
);
RouteTable.Routes.MapHttpRoute(
name: "ApplePassPassApi",
routeTemplate: "{version}/{controller}/{passTypeIdentifier}/{serialNumber}"
);
RouteTable.Routes.MapHttpRoute(
name: "ApplePassLogApi",
routeTemplate: "{version}/{controller}"
);
}

Test

Test the API in Python3:

import requestsrequests.post('https://[example.com]/v1/devices/deviceLibraryIdentifier1122334455/registrations/pass.TypeIdentifier.6677/serialNumber_99',  data = {'pushToken': 'pushToken_10'}, headers={"Authorization": "ApplePass 444555562343"})requests.get('https://[example.com]/v1/devices/deviceLibraryIdentifier1122334455/registrations/pass.TypeIdentifier.6677?passesUpdatedSince=2018-1-1',  headers={"Authorization": "ApplePass 444555562343"})

If everything works fine, you will see a 200 response.

Database Table

//SQL SERVER
CREATE TABLE ApplePassDevices (
deviceLibraryIdentifier varchar(100) NOT NULL UNIQUE,
pushToken varchar(100),
updateTimestamp datetime
);
CREATE TABLE ApplePasses (
passTypeIdentifier varchar(100),
serialNumber varchar(100),
updateTimestamp datetime,
passDataJson varchar(max) --Used for telling if Passes were out of date
);
ALTER TABLE dbo.ApplePasses
ADD CONSTRAINT uq_ApplePasses UNIQUE(passTypeIdentifier, serialNumber);
CREATE Table ApplePassRegistrations (
pkid int IDENTITY(1,1) PRIMARY KEY,
deviceLibraryIdentifier varchar(100),
passTypeIdentifier varchar(100),
serialNumber varchar(100),
updateTimestamp datetime
);
CREATE Table ApplePassAPILog (
pkid int IDENTITY(1,1) PRIMARY KEY,
APILog varchar(5000),
timestamp datetime
);

Some issues I faced…

If the DELETE request wasn’t allowed, add below codes to web.config:

<handlers>
<remove name="ExtensionlessUrlHandler-Integrated-4.0" />
<add name="ExtensionlessUrlHandler-Integrated-4.0" path="/v1/*" verb="GET,POST,DELETE" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>

Also, the period in URL causes a 404 error for GET request. For example, GET “v1/Devices/passTypeIdeentifier/registrations/passTypeIdentifier.pass”. You can add a backslash to the url to solve the issue. [ref1, ref2]

I have part 2 here to describe when something changed on server, how to update Passes in the Wallet.

~Happy Coding! :)~

--

--