PassSource has been helping people create Apple Wallet passes since they were announced by Apple. We've had lots of experience with different industries and users and have identified several features that are unique to PassSource that allow adding significant power and capabilities to our templates without having to write a single line of code. Our API was always designed to be easy to use and test and we've extended this philosophy to our advanced features that are available to our professional accounts.
PassSource uses templates to create prototypical passes from which passes can be created. Changes to the template will propagate to passes when they are updated (if no update is sent or requested, they won't see the change). If a pass has overridden the template value and the original template value changes, this will not change the override (i.e. it will still be overridden with the pass value).
PassSource templates have some advanced features unavailable anywhere else that allow powerful workflows without having to write any code. You can use these features with or without using the API and many of the features eliminate the need to use the API.
Most of the attributes of a pass are exposed via our API Paths which are used to identify values and fields. These are used for the PassSource advanced features as well as in API calls.
You can find the API path for a field in the template editor. Click on the (i) button to the right of a field to get advanced options and see the API path.
The path for additional properties are simply the key.
In addition, we've made it easier to change the pass type and the transit type by exposing the following paths:
type
Changes the type of pass. Valid values are boardingPass
, coupon
, eventTicket
, generic
, storeCard
.transitType
The transit type for a boardingPass. This is required if the type is set to boardingPass
and is ignored otherwise. Valid values are PKTransitTypeAir
, PKTransitTypeBoat
, PKTransitTypeBus
, PKTransitTypeGeneric
, PKTransitTypeTrain
.We've implemented something we call "magic" strings which is a fancy way of saying, anytime we have a string in a pass, we automatically look for certain special strings and automatically replace the values when creating the pass.
You can use any API path as a magic string by surrounding the path in curly brackets. For example, if you wanted to include the points balance in some text on a backfield, you could include {structure_headerFields_points_value}
in the text, and when the pass is created, it will be replaced with whatever value is in the structure_headerFields_points_value
field. Or if you wanted to have the user's name encoded in the barcode message, you might set the barcode_message value to {structure_secondaryFields_name_value}
Note that to prevent users from using magic strings in registration fields, curly brackets are escaped. However, if you're using the API to set these values, they will not be escaped if you use your clientHash
to authenticate the edit.
In addition to paths, we also have some special strings that are available to use:
string | description |
---|---|
rootPath | This is replaced with the PassSource URL (currently https://www.passsource.com ). This is useful if you are switching between the beta environment or if you're using our domain masking service or just so you don't have to worry about typos with the URL. |
clientHash | This is a token used for authentication. Protect this as you would your password as it can be used to grant full account permissions. Typically, you would only use this within the pass for special additional PassSource features like the psInformationURL in the scanning feature, on other internal pages, or when using triggers to call the API and this should never be included in any pass fields (we do allow this for testing and edge cases so be careful). |
clientId | If you want the user to go to branded white-label pages, you may want to provide your client ID. This is much safer to include than the client hash as there is nothing that can be exposed other than public branding information. |
templateHash | The template hash for the pass. Typically used if you are creating a link to a new registration page for a share link or if you're executing triggers that call API functions that require the template hash. |
hashedSerialNumber | The hashed serial number to this specific pass. This is often used to link to the create or registration page and used whenever referencing a specific pass instance in the API. |
timestamp | Set to the current timestamp in ISO 8601 time format (GMT time). Can be used for example to set a membership start date in an onRegister trigger. |
psPassRegistered | Resolves to boolean true if the pass has been registered and false otherwise. Can be used in conditions or exports. |
psPassInstalled | Resolves to boolean true if the pass has been installed and false otherwise. Can be used in conditions or exports. |
psPassScanned | Resolves to boolean true if the pass has been scanned using our scanning feature or the API and false otherwise. Can be used in conditions or exports. |
psPassDeleted | Resolves to boolean true if the pass has been deleted and false otherwise. Can be used in conditions or exports. |
psPassURL | A convenience path that maps to {rootPath}/pass/create.php?hashedSerialNumber={hashedSerialNumber}& . |
psRegisterURL | A convenience path that maps to {rootPath}/pass/register.php?hashedSerialNumber={hashedSerialNumber}& . |
psEditorURL | A convenience path that maps to {rootPath}/pass/editor.php?hashedSerialNumber={hashedSerialNumber}&clientHash={clientHash}& . Be mindful when using this as this will contain your clientHash which can be used to update passes and templates and gives full access to your account. Use this only with API calls or as part of triggers as this should not be included on the pass itself. If psEditCode is in the template, instead of the clientHash, it will include the templateId and the psEditCode which is better if you want to restrict access to a specific template. This is a convenience path for setting psInformationURL for the scanning feature but should not be used if using the psAllowOpenScan option and not using the psEditCode as this could expose your clientHash or psEditCode. If this is used with :PassScannerConfig or in the psPortalHeader , it will not include the hashedSerialNumber . |
psScanURL | A convenience path that maps to "{rootPath}/pass/scan.php?clientHash={clientHash}&". This is a convenience path for setting up the Pass Scanner app with a custom URL. Be mindful when using this as this will contain your clientHash which can be used to update passes and templates and gives full access to your account. Use this only with API calls or as part of triggers as this should not be included on the pass itself. If psEditCode is in the template, instead of the clientHash, it will include the templateId and the psEditCode which is better if you want to restrict access to a specific template. |
psPublicScanURL | A convenience path that maps to "{rootPath}/pass/scan.php?hashedSerialNumber={hashedSerialNumber}&" (without any authentication). Set the barcode message to this to use our public scanning feature. |
psScannerConfigURL | A convenience path that expands to {psScanURL:PassScannerConfig} for configuring the Pass Scanner app. Be mindful when using this as this will contain your clientHash which can be used to update passes and templates and gives full access to your account. Use this only with API calls or as part of triggers as this should not be included on the pass itself. If psEditCode is in the template, instead of the clientHash, it will include the templateId and the psEditCode which is better if you want to restrict access to a specific template. |
PSColorGreen | A convenience constant for our default approved scan color. Currently resolves to rgb(77,186,11) but may be changed in the future. |
PSColorYellow | A convenience constant for our default warning scan color. Currently resolves to rgb(187,187,58) but may be changed in the future. |
PSColorRed | A convenience constant for our default error scan color. Currently resolves to rgb(187,49,58) but may be changed in the future. |
A note on hashes: If a hash value is used at the end of a URL, add an ampersand (&) at the end if you include this in a back field or in an email our hashes may end with a comma (,) which may not be included in link detection and may cause problems.
Magic strings also support some special transformations. For example, to get the URLEncoded value for the scan URL, you could use {psScanURL:URLEncoded}
.
string | description |
---|---|
:Ternary | ,trueValue,falseValue Similar to a ternary operator in many programming languages, allows displaying one value if the root value is true and another value if the root value is false. Can use variables in place of inline strings (but cannot include additional levels of magic strings). Example usage: {voided:Ternary,"Pass Voided","Pass Good"} or {booleanTrue:Ternary,"YES","NO"} |
:URLEncoded | Performs a URL encoding on the string before replacing. Useful when specifying a value as a parameter in a URL. |
:Trimmed | Trims whitespace from either side of the string. For example, you could set up a field fullName with the value "{firstName} {lastName} " and use {fullName:Trimmed} to ensure there aren't extra spaces should the user only have a first or last name. |
:StripWhitespace | Removes all whitespace from the string. Great if you want users to enter a barcode number with spaces for readability but the actual barcode should not include spaces. |
:EscapedQuotes | Escapes double quotes in the string by adding a backslash before the double-quote. |
:DateFormat | ,"format string" Converts a date into the desired format. Format string should use the PHP date format options. You can use "c" to specify the ISO 8601 format that needs to be used for things like expiration dates or event dates. Note format string cannot contain line breaks or curly brackets. |
:Pad | ,numberOfCharacters,"padString",padType Pads the base string with the padString to the numberOfCharacters length. padType is an optional integer and will be left if < 0 (the default), right if > 0, and on both sides (centering the text) if padType = 0. |
:PassScannerConfig | Generates a URL for configuring the Pass Scanner app (sets the customURL parameter to the value stored in the key). This works with our convenience URLs like psScanURL or psEditorURL and maps to "passscanner://?customURL={KEYED_VALUE :URLEncoded}" as well as including configuration to match the first beacon if specified. |
Our famous registration pages have gotten even better! You can create dropdowns and checkboxes for users to fill out and additional fields can also be set up for user registration. Validation is performed on registration fields to ensure that required fields are filled out and in the correct format. When the user registers, we automatically flag the pass as registered and execute any onRegister triggers you have set up. Further, you can customize the registration page and the registered pages using PassSource additional fields.
To make fields user editable, in the template editor, click the (i) button next to the field and check the User Editable box.
To enable registration, you will have to provide users with the templateHash or make the template public. Having the templateHash without the clientHash only allows users to edit user editable fields. If no user editable fields exist, the registration page will automatically re-direct to the creation page.
You may automatically pre-fill user editable fields by setting the values as GET parameters in the URL to the creation page. This page also supports sending the values via POST variables or encapsulated as a JSON string passed to a JSON variable.
To take people to a registration page to update their pass, simply direct them to {psRegisterURL}
on the back of the pass. Use the psRegistrationMessage
and psRegisteredMessage
additional properties to customize the registration and registered pages.
For example, link to the registration page by creating a backField
with the attributed value set to:
<a href="{psRegisterURL}">Register your pass!</a>
and the normal value to:
Register your pass by tapping this link: {psRegisterURL}
Note that if you assign the value to a container path using the API (for example, setting the barcode
or structure_headerFields
), and any of the sub-fields are user editable, accessing the registration page of a pass will automatically assign the user editable values and remove the override value of the pass so that the user can edit the information again.
You can add validation to user editable fields. Validation will use one of our pre-coded validators to verify that the user-submitted information is valid. The Validation field should be a comma separated list of validators without any spaces. With the exception of VRequired, the validators are not applied to an empty string.
The available validators are:
validator | description |
---|---|
VRequired | Makes sure the value is set to something. |
VEmail | Checks to make sure the value is a properly formatted email address |
VDate | Must be a properly formatted date, but can be anything PHP stringtotime function will interpret. |
VPhone | A properly formatted phone number. This must be only a numeric number but +,x() characters are okay. |
VNumeric | Must be able to be converted to a number. |
VAlpha | Only a-z and A-Z are supported. No special characters or numbers. |
VAlphaNumeric | Only a-zA-Z0-9 are supported. |
VDecimal | Must be able to be parsed as a decimal digit. Uses PHP is_float function. |
VInteger | Must be an integer number. |
VStateCode | Must be a valid US State code. |
VZip | Must be numeric (but can have - ) and be 5-10 digits (not including the - ) |
VTrue | Must evaluate to boolean true (typically used with Checkboxes). |
VFalse | Must evaluate to boolean false (typically used with Checkboxes). |
We've always had some simple tools for editing passes from our Pass Scanner app, but now it's even easier to customize this page for your individual needs. Simply enter the API paths to the fields you want to edit as an additional field named psEditorPaths
.
You can get to the pass editor via the following link: https://www.passsource.com/pass/editor.php?hashedSerialNumber={hashedSerialNumber}&clientHash={clientHash}
(You can use the templateId and editCode instead for more limited authentication). We suggest including this path as the value of the psInformationURL
additional field when using the scanning feature (the clientHash
is safe to include here if you're not using the public scan feature. You can also use {psEditorURL}
magic string to automatically expand this URL and it will include the client hash or the templateId and psEditCode
if set).
Professional accounts now have an easy way to view template stats and drill down to lists of individual passes. You can even expose fields here for quick template updates to push to all the passes. For example, if you have a marketing message you edit to push notifications to users, you could expose that field here for quick editing without going through the template editor. Similar to the editor, simply enter the API paths to the fields you want to edit as an additional field named psPortalUpdatePaths
. Note that unlike the editor, these updates will be applied to the master template and then a push update will be sent to all installed passes.
We have a tool for quickly creating multiple passes by uploading a CSV file. The file should contain a templateId
column indicating the ID of the template you'd like to use. The other column headings should be API paths that you want to set in the template.
Please make sure the CSV file is UTF-8 encoded, does not contain any blank rows or hidden characters, and make sure the column names do not contain whitespace and match the capitalization of your API paths exactly to prevent any issues.
By default, the tool will generate the creation links you can send via email so that passes are only created when the link
is visited. The fields in the spreadsheet will be validated to make sure they actually exist as user editable fields since that is required for dynamic links to work. You can also choose to use our link shortener to make it harder for end users to tweak the links.
You can also create the passes ahead of time which will append a hashedSerialNumber
and psPassURL
column to each row for use in an email tool or database or for use with our API calls. You can also simply specify a number of passes to create from a template and we will generate a list of hashed serial numbers.
If you want an even shorter link, you can replace "https://www.passsource.com/h
" with "https://pasz.us
".
We've provided an example file that modifies the logoText, barcode_altText, and background color for a generic pass (you will need to copy the generic pass and use your template ID to test).
One final note: If you're using non-ASCII characters (for example accented characters or chinese characters), be sure that the file is encoded in UTF-8 format or the tool may stop midway through. If you notice a different number of passes in the export file than you expect, check the row after the last row you see and check for special characters.
Professional accounts now have an easy way to export the pass data for a template. It's flexible so that you can get exactly the data you need and you can build up custom fields and information using our additional and path features. There will be a Export Data button on the template portal if a psExportPaths
additional field is present. This will download a CSV file with the requested data with one row per pass. Note that for performance reasons, the tool will only download in batches of 1000 and will order based on creation. To fetch more than 1000, add &start=START_INDEX to the URL where START_INDEX is the row you want to start with (for example, to fetch page 2, set START_INDEX to 1000).
If you have a reseller account, you can specify a logo and color that will be used to brand the entire site. Pages will not include any PassSource branding and ads will be hidden. To see an example, add &clientId=6
to any page to see what it looks like with our demo account branding. If you're interested in setting up a reseller account, please contact us.
The Additional JSON option on the Advanced Settings page allows you to specify additional variables available to all your templates. Settings are specified as a valid mapping of keys and values in JSON object notation. Note that these values will always override anything specified at the template or pass level. You may also use this to specify additional customizations and options for your account:
key | description |
---|---|
psDashboard | Should be a JSON object map of labels to URLs. This will add additional menu options on the client's dashboard (useful for adding quick links in your own account). If the URL does not start with http, it will automatically be prefixed with {rootPath} . |
psHelpTitle | Customize the help page title. |
psHelpInfo | Customize the help page content. |
psLoginHeader | Customize the login screen. |
psScanErrorText | Mask any error while scanning with a single string (for example to replace our default English error messages with a different string). |
pageTitle | Sets the title metadata value which is shown in link previews and browser headers. |
siteDescription | Sets the header metadata description tag for the site. |
Apple Wallet fields and the barcode can be set to display conditionally based on certain criteria. The field/barcode will only be included on the pass if the conditions evaluate to true
. This can be useful if you only want something to be on the pass some of the time, but not all the time. For example, you might want to show a deal only after the pass has been scanned.
To specify conditions, click the (i) button for the field to disclose the advanced options and you will see a PassSource Conditions
field.
Conditions can also be used to selectively execute triggers.
You may specify conditions using the following grammar:
grammar | notes |
---|---|
<boolean> → AND | |
<boolean> → OR | |
<comparator> → !<comparator> | allows testing X <= Y by doing !(X > Y) |
<comparator> → = | |
<comparator> → > | |
<comparator> → < | |
<test> → isEmpty | is 0 or false or "" or null |
<test> → isSet | is not null |
<test> → !<test> | |
<magic> → {magic} | a magic string that will be evaluated and the evaluated value will be used for tests |
<valueString> → <magic> | |
<valueString> → a number | |
<valueString> → a boolean value | |
<valueString> → a string value | a string should be denoted by surrounding in double quotes |
<conditionString> → <valueString> | |
<conditionString> → <valueString> <test> | |
<conditionString> → registered | scanned | deleted | installed | registered , scanned , deleted , and installed are special keywords that are evaluated only if present (this has been deprecated since there are now magic strings for these 4 states) |
<conditionString> → !<conditionString> | for negating a condition |
<conditionString> → (<conditionString>) | for grouping conditions and nesting evaluation |
<conditionString> → <conditionString> <boolean> <conditionString> | for joining multiple conditions |
<conditionString> → <valueString> <comparator> <valueString> | if either valueString is a string, the values are compared as strings, otherwise they are compared numericly |
See below for examples.
Triggers are a powerful new feature exclusive to PassSource that allows you to build complex workflows without writing a single line of code. Triggers allow you to hook into events like pass registration, scanning, and updates. PassSource logs these events for the lifecycle of a pass, and with triggers, you can get pass updates, make changes to a pass, or more deeply integrate with a custom service exactly when you want to. Currently, the trigger action is defined as a JSON dictionary, but we will be adding a more friendly visual editor in the future.
The following is a list of access types and their corresponding trigger keys:
Trigger and Value | When Logged |
---|---|
onCheck 0 | Triggered by a call to the trackPassAccess API (which is called by the Pass Scanner app). |
onCreate 1 | When the serial number is generated for a pass (guaranteed to execute exactly once per pass). |
onInstall 2 | Logged by the web service whenever a pass is installed on a device. Repeated installs or installs on other devices will log additional entries. Triggers onInstall triggers regardless if the pass has already been installed. |
onDeviceUpdate 3 | Logged when user invokes a manual update by doing the pull-to-refresh on the back of the pass. To prevent issues, additional requests within 30 seconds are ignored. |
onRegister 4 | This is called if the registration form is submitted. Add the condition !registered to only execute the first time the registration form is submitted. |
n/a 5 | Logged by the updatePassesForTemplate and pushPassUpdate API calls. The former is used by the "Update All Passes" button on the template editor and the template portal when submitting exposed fields. There are no associated triggers as this is not a user-driven event. Also, will not be logged if the update includes more than 100 passes. |
onDelete 6 | Logged when the user deletes a pass from a device. Note that there is no guarantee that this will be called when a pass is deleted and should not be depended on for determinining if a user has a pass installed or not. |
onScan 7 | Triggered by a call to the trackPassAccess API (which is called by our scanning feature). |
onEdit 8 | Triggered when submitting the editor form. Use to automatically process pass before sending updates to the user. |
When a trigger is encountered, it looks through the template for a set of actions and executes them in order. Each trigger action should be a dictionary with an action
key and some additional keys depending on the action. The following lists the valid actions and thier properties:
Action | Additional Keys and Description | ||||
---|---|---|---|---|---|
PSActionFetchURL |
resultsPath is specified. If there is a problem, use the API to set the psTriggerError additional value of the pass to a string. If this is set, no further triggers will be executed and the event will not be logged. If this is done in an onRegister trigger, the error will be displayed to the user.
| ||||
PSActionSetValue |
targetPath to value .
| ||||
PSActionIncrementValue |
amount . The field will be forced to a number if it is not already. You can also use this to decrement a field by passing a negative value.
| ||||
PSActionExecuteTriggers |
|
PSConditions
key to specify conditions that will be evaluated to determine if trigger is executed or skipped.
To see an example of how a trigger action JSON dictionary is written, check out our examples. A simple one changes the color of a pass when scanned.
Our templates allow the setting of additional properties that are not included in the pass itself so that you can have hidden/private values.
Additional fields have a key and a value which can be set to be user editable. The key can be used wherever we use API paths but the values are generally hidden from users. You can use these fields to build databases and applications on top of the PassSource platform without having to host your own database and when combined with PassSource Triggers, many common workflows can be created without any custom code or services.
You can add any keys except keys that are already used by the pass (such as barcode
, backgroundColor
, logoText
, coupon
, etc.). They must be unique which is why we suggest using prefixes to identify your keys.
We have pre-defined several special keys that add additional functionality if set as additional parameters:
key | description |
---|---|
psPassDescription | If set, this will be used in pass lists and on the editor page. If this is not set, we'll set it for you, so you can use this value even if it isn't set up as an additional field. This is also used as the descriptive label in the Pass Scanner app. |
psPathMigrations | A JSON object mapping old paths to new paths so if you decide to move structure_auxilliaryFields_myField_value to structure_secondaryFields_myNewField_value , you can specify this map so user's data will properly migrate when updated or fetched: {"structure_auxilliaryFields_myField_value": "structure_secondaryFields_myNewField_value"} |
psIndexPaths | A comma separated list of paths that also serve as a key to ensure that passes created are unique. Attempting to create a pass with the same value as an existing pass with the same index value will result in the original pass being returned instead. |
psRegistrationMessage | To further customize the user's registration experience, this value will appear at the top of the registration form and may include HTML. |
psRegisteredMessage | This appears on the page that appears after the user registers or updates their pass and may include HTML. Set this value in an onRegister trigger to provide custom messages based on the user's registration data. |
psEditorPaths | A comma-separated list of paths to use with our editor tool. We suggest setting the psInformationURL to {psEditorURL} to use this editor feature with our scanning feature. |
psEditCode | Allows specifying a code that can be used to edit passes for a specific template without having the clientHash. If this is specified in the template, convenience URLs (like psEditorURL ) will include this and the template ID instead of your clientHash to restrict authentication based on a template. Having the psEditCode and the template ID, someone could edit the values of a pass but cannot make changes to the template or edit the values of passes based on other templates. If this is present, there will be an additional "Configure Pass Scanner App" button in the PassSource Settings for the template. |
psEditorHeader | HTML text to customize the appearance of the editor page. Defaults to <h2>{psButtons}{psPassDescription}</h2> (note psButtons is not a normal magic string but is replaced with the appropriate buttons for this page. |
psPortalHeader | HTML text to customize the appearance of the portal page. Defaults to <h2>Template Information</h2> |
psPortalUpdatePaths | A comma-separated list of paths that you want to expose on the portal page for quick updates to the template. It's like your own personal registration form for the template! |
psExportPaths | A comma-separated list of paths to use with our Batch Export tool. |
psLoginHeader | HTML text to customize the login screen for the public scanning interface. |
psTriggerError | Set this value to a string to stop execution of any triggers, not mark the pass as registered, and display the error to the user. The user will see this and can go back to the registration page to correct the error, so provide a useful message. This field will automatically be cleared and reset at the beginning of any trigger execution and can be set without being set up as an additional field in your template (in fact, you should not set this in your template unless you are testing and want triggers to always fail). |
We have developed a robust scanning system that allows quick setup in many different types of workflows. While you can build your own scanner or use our API to integrate with virtually any system, we also offer some solutions for situations where a system does not exist.
We offer various ways to scan our passes. By default, the PassScan QR code is simply a link to create a copy of the pass which allows it to be printed on physical cards and paper so that it can be scanned by a configured scanner or added to a wallet app.
If you do not have scanning hardware, we offer our Pass Scanner app for iOS which has multiple features including broadcasting an iBeacon signal.
We also offer a way to use 3rd party QR code scanners (including the one built into iOS 11 and later) to trigger scans using our psPublicScanURL feature. This allows you to set the barcode_message
to {psPublicScanURL}
and any QR scanner that can open URLs can be used as a scanner! Any onScan triggers will not fire and no scan text will be displayed unless 1) the scanner is authenticated using the account login, 2) the scanner is authenticated using the psEditCode
feature, or 3) you have set psAllowOpenScan to true
.
As long as the scanner app supports cookies, it will remember this authentication between scans. If you do need to remove a scanner's access, simply change the template's edit code or your account password if not using an edit code.
The user interface for both our Pass Scanner app and the psPublicScanURL feature can be customized for quick integration and onboarding. The background color (psScanColor
) and displayed text (psScanText
) can be set via triggers or in the template or using conditional magic string values.
An additional button can be presented for more information (label: psInformationLabel
, url: psInformationURL
).
Our UI can be bypassed entirely and instead show an alternate URL (psDisplayURL
) while still triggering any onScan
triggers and using our authentication services.
Add these additional parameters to your template to configure the scanner interface (or you can use our API to create your own custom user interface):
key | description |
---|---|
psScanColor | A color value for the background (we recommend a dark color as the text will always be white). |
psScanText | The text to display when scanned. This can be set using our onScan triggers or you can use a Magic String function like the :Ternary operator to conditionally display messages. |
psInformationURL | If present, a "more information" button will be displayed. |
psInformationLabel | This will override the default text of the "more information" button (which defaults to "More Information". Early versions of our Pass Scanner app do not support this feature. |
psDisplayURL | A url to a page that will be displayed after any onScan triggers are processed. Can be used to provide a custom scan interface while still using other features like triggers and public scan authentication. |
psAllowOpenScan | If this is set to true , any public scan authentication will be bypassed to allow anyone to trigger any onScan triggers (this should not be used if you require any sort of validation or verification as the results could be faked). |
psEditCodePrompt | The text to be displayed if psEditCode is present and using the psPublicScanURL feature. Defaults to simply displaying the same error that would normally be displayed. |
Our Pass Scanner app (free on the App Store) can be used as an end-to-end solution to scan and manage your Apple Wallet passes.
You can go into the app, tap the ?!
Button, and select the various options.
The app can be set to display the encoded data (for copying or simply viewing), or it can pass the barcode data as a get parameter (message
)to any URL (like our scan URL), or you can enter your PassSource clientHash
to authenticate.
When using the custom URL mode, a GET
parameter message
will be added to the custom URL with the contents of the barcode data as a string. The app then opens a web-view with that URL where you can perform custom actions or create your own interface.
You can automatically configure and authenticate the app using the button on your advanced settings page.
To automatically configure custom URL mode with any URL, you can visit the following configuration URL on the device with the Pass Scanner app installed:
passscanner://?customURL={your custom url URL encoded}
The flash in the app turns on automatically if there isn't enough light. Make sure the phone is not resting on the table when you start the app and there is enough light in the room and the flash should not engage.
The app is designed not to go to sleep while the scanner is visible, so we recommend keeping the device plugged in if you are going to set this up near a register.
In PassScan mode, it will automatically return to scanning screen after 5 seconds unless you tap anywhere on the screen.
To automatically set the iBeacon parameters for the app, use a URL in the format:
passscanner://?proximityUUID={proximityUUID}&major={major}&minor={minor}
You can use this link to set up the app to be a a beacon for our demo passes.
Getting started with the API is simple!
See our examples and tutorials for some ideas.
This is the easiest way to make changes to templates without having to worry about the "API". In fact, this was the primary method for making changes and is still supported with the old login API, the new login API, or the clientHash.
You can send links to users with their information pre-filled so that you don't have to pre-generate passes.
This method is particularly useful if you already have an email program that you can insert fields from your database into. It does not require any web server or backend to manage your passes so it is the easiest solution for restaurants distributing loyalty cards or coupons. Only user-editable fields can be overwritten, so it's easy and convenient way to pre-fill data for users. If you don't want users to be able to edit their data, you will probably want to use the API to pre-generate fixed passes instead.
Before you create your pass link, you will want to get the templateHash. This can be found on the template editor under PassSource options.
Once you have your template hash, to create the pass link, just construct a link as follows:
https://www.passsource.com/pass/create.php?templateHash={templateHash}&{apiPaths}
The apiPaths are any number of key value pairs where the key is an API path and the value is the URL encoded value to set.
You may also overwrite and specify images by using the key images_
plus the image name and the value to a URL encoded URL to the image. For example, you could add the following to change out the logo:
&images_logo=http%3A%2F%2Fcreativebits.org%2Ffiles%2F500px-Apple_Computer_Logo.svg_.png
The possible image keys are as follows. Note that not all pass types support all image types. For more information, check out Apple's documentation (you may need an Apple developer account to access).
images_icon
images_logo
images_thumbnail
images_strip
images_background
images_footer
You may add "@2x
" to the end of any of those to specifically target the high resolution version and we recommend setting both the normal and the @2x versions. We will automatically scale the images to be the optimal size, so you can use the same path for both the @2x and normal versions.
You can also pre-generate passes and then link to the pass using the hashedSerialNumber. This is a good method for creating store-cards and other items where you would rather the user not be able to modify the values themselves. The first step is to add the user's custom fields in the same manner as the clear-text pass links and include your clientHash or simply use the API. If you're using the create page, you can add &generate=true
to the URL to have it show the hashedSerialNumber instead of a page with the pass or downloading the pass. However, we recommend using the createPassFromTemplateFields API call as you will be guaranteed a JSON response even if there is a problem.
The hashedSerialNumber is a string representing the serial number for this pass. You can store this in your database as you will use this for any updates and tracking of this pass. When you want to give this pass to the user, simply direct them to the following URL:
https://www.passsource.com/pass/create.php?hashedSerialNumber={hashedSerialNumber}
This link will display a page with the pass and a link to download unless they are on an iPhone or iPod touch in which case it will simply prompt the user to add the pass to their Apple Wallet.
Why hash the serial number? The serial number is not secured for the user since they can obviously open up the pass file to get that. The reason we hash the serial number is it adds an additional level of complexity. Since anyone can download any pass by just having the serial number, hashing it makes brute-force guessing of serial numbers that much more difficult.
Typically you'll be linking to passes using the hashedSerialNumber as passing around large data files isn't very efficient, but if you have an iOS native app or you have to have the pass data, you can download the Apple Wallet pass file.
To download the pass file, add &download=true
to the create URL. This acts the same as the generate
parameter with the difference that instead of returning the serial number, it will return the pass itself. If you need the hashedSerialNumber, do the generate command and then use the user's create link with the download
parameter to get the pass data.
We now have offer a stateless API that uses a token for authentication instead of requiring logins and cookies. We call this authentication token your clientHash
. This is a unique key that is specific to your account and is used in all your API calls to validate that you have permission to perform the action. It is based on your account ID, email, password, and subscription status, so if any of these values change, so will your clientHash
(so if you ever need to invalidate a client hash, simply change your password). It is important that you do not share your clientHash
and make sure it is not publicly viewable by not including the magic string "{clientHash}" in public/visible fields in the template.
You can use the login API function to get your clientHash
, or you can simply copy it from your advanced settings page.
Pretty much everything is based on templates. You can use our website to create templates to customize passes to your liking. This will be your first step in designing and testing passes you will customize for your users.
Go ahead and create some templates and try out the passes on your test device to see how they look. Once they look right, you can move on to creating passes for your users.
Public passes are branded with a link back to PassSource.com and are shareable.
We have designed our API to be simple to use and simple to test. We wanted to allow companies to send an email blast that could contain links with embedded personal information to easily create personalized passes for their users. We also wanted developers to be able to easily test the API with just a web browser without having to write complicated code or use any specialized frameworks.
API functions can be called by simply constructing a URL with the following format:
https://www.passsource.com/api/{apiFunctionName}.php?clientHash={clientHash}&{parameters}
Most of the API calls expect data to be passed as simple HTML GET parameters, however, for functions that accept large amounts of data, you can also send a JSON encoded object with the parameter keys as a string via HTML POST field with the key "JSON" (this is also useful if you have any array data that is not easily flattened). Functions that support POST upload are indicated in the table. The clientHash
and templateHash
parameters should always be sent via GET for authentication.
All API calls return a JSON object with at least a success
key that may be true or false. If it is false, there will be a key message
that will contain the error message. If you are testing and want formatted instead of compact JSON, add a GET parameter prettyOutput
to format the JSON (this only happen for short JSON responses). If there is an error, please handle it appropriately. If you ignore the error and continue to generate errors using the API, we will lockout your API usage for a period of time to minimize server resources. As long as you work to prevent errors, this should not affect most people's normal API usage and has been built to catch potential runaway scripts or coding errors. If you encounter this and need assistance, feel free to contact us.
Certain fields need to be in a very specific date+time format or the pass will not install. If you use our interfaces, we'll do our best to validate and force your data to the proper values, however, if you use the API, you may be able to construct a pass with an invalid date format causing the pass to not install. If you have XCode installed, you can connect an iOS device and view the logs to see what the exact error is, but if it's a date error, it will look something like this:
Unable to parse relevantDate 2017-10-17T00:00:00 as a date. We expect dates in "W3C date time stamp format", either "Complete date plus hours and minutes" or "Complete date plus hours, minutes and seconds". For example, 1980-05-07T10:30-05:00.
function | parameters, description, and returns | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
login |
| ||||||||||||||||||||||||||||||
logout | Logs you out and clears the session cookies. Client hash not required. | ||||||||||||||||||||||||||||||
clientInfo |
| ||||||||||||||||||||||||||||||
clientTemplates |
url key that will point to the template portal page.
| ||||||||||||||||||||||||||||||
templateInfo |
| ||||||||||||||||||||||||||||||
trackingInfoForTemplate |
| ||||||||||||||||||||||||||||||
passesForTemplateMatchingAccessType |
| ||||||||||||||||||||||||||||||
updatePassesForTemplate |
| ||||||||||||||||||||||||||||||
insertUpdateTemplate |
POST data supported (clientHash and templateHash parameters should be sent via GET)
| ||||||||||||||||||||||||||||||
updateClientAdditional |
POST data supported (clientHash parameter should be sent via GET)
replacedAdditional with the previous additional value that was replaced. | ||||||||||||||||||||||||||||||
updateTemplateFields |
POST data supported (clientHash and templateHash parameters should be sent via GET)
| ||||||||||||||||||||||||||||||
uploadTemplateImages |
POST data supported (clientHash and templateHash parameters should be sent via GET)
| ||||||||||||||||||||||||||||||
deleteTemplate |
| ||||||||||||||||||||||||||||||
createPassFromTemplateFields |
POST data supported (clientHash and templateHash parameters should be sent via GET)
psIndexPaths set and a matching pass for an index path exists, that pass will be updated instead of creating a new pass.
| ||||||||||||||||||||||||||||||
passDataForHashedSerialNumber |
| ||||||||||||||||||||||||||||||
trackingInfoForPass |
accessType key and a time key.
| ||||||||||||||||||||||||||||||
trackPassAccess |
accessType is a check or scan, the return json may include the following keys:
| ||||||||||||||||||||||||||||||
checkPassAccess |
| ||||||||||||||||||||||||||||||
updatePassFields |
POST data supported (clientHash and hashedSerialNumber parameters should be sent via GET)
| ||||||||||||||||||||||||||||||
pushPassUpdate |
| ||||||||||||||||||||||||||||||
deletePass |
| ||||||||||||||||||||||||||||||
imageForNameTemplateHash |
Either provide the templateHash or the hashedSerialNumber
|
Please note that we've made a lot of upgrades since our initial release of PassSource in 2012. We've done our best to maintain backwards compatibility with previous versions of our API, but if you find that we missed something, please let us know! Over time, we will eventually remove the older API hooks, but we've checked how people are using things and most of the things we've deprecated weren't being used and we've replaced them with more generic and powerful features.
We've re-written the entire back-end to be much more modular, efficient, and flexible with increased whitelabel support across the board. We have even created some PHP classes you can drop-in to your code to make things even easier (which was quite a feat as our API was already probably the easiest out there). One of the main reasons for the new APIs was to make custom solutions easier for us and our clients and most of the new site has been built on top of the API, so it should be fairly comprehensive.
We've also completely re-designed our front-end to better match newer design sensibilities and to allow complete re-branding of most of the site should you need to whitelabel.
We have simplified things across the board and made things consistent and made the most requested features even easier to implement via new dashboards and portal pages.
We will demo new features on our new beta site before pushing changes live, so if you ever need to test in a playground environment, you can use that server to make destructive edits or test the API or features. Periodically, we will replace the account and template data on the beta server with the live server values, however, passes and tracking may not be synced. Feel free to contact us about the use of the beta server. The beta site can be found at: https://beta.passsource.com
Tracking data and passes that haven't been updated in over 2 years may automatically be purged from our system. Additionally, passes that have no device or access tracking data (passes that have not been accessed or installed besides their initial creation) will be purged after 6 months.
If you think this may be an issue or if you have a pass that may have been purged that you need, please contact us.
You can use our PassSource.php classes to update templates or you can update the code manually via the API.
This part of the API starts getting pretty complex, so if you'd like our help in doing something, please drop us a line.
All templates are built on a template structure which is simply a JSON object that defines information about a pass and indicates user editable fields. It's purposefully designed to be very similar to Apple's pass JSON structure and in fact you could take the manifest.json from a pass to create a new template.
A field can consist of the value or, more commonly, will be a PSAttribute JSON object which will have the following keys:
PSValue | The actual value. |
PSEditable | WILL BE DEPRECATED! If you are using this, let us know and we'll make sure your code migrates. We will be moving to a psRegistrationPaths field similar to psEditorPaths to allow for ordering of fields which won't be compatible with PSEditable. This will also add some additional consistency. Currently, this will indicate that this field should appear on the registration form and can be set via URL parameters. Editable fields may have the following additional properties: |
PSLabel | This will be used when the field is displayed on the registration, editor, or portal forms. By default, this will be the path to the field. |
PSAttributeType | By default, most fields are just text, but you can have special editors. Currently we support PSAttributeTypeText, PSAttributeTypeDropdown, PSAttributeTypeMultiselect, PSAttributeTypeCheckbox, PSAttributeTypeColor, PSAttributeTypeStepper. We will be adding a date picker and time picker and location picker soon. |
PSOptions | You can specify a set of options that will appear as a dropdown on the registration form. They "keys" of this JSON dictionary should be the labels and the "values" should be the value that will be set. |
PSStepAmount | If the PSAttributeType is PSAttributeTypeStepper, this value indicates the amount to increment or decrement by. This is mainly for conversion from the legacy PassScan increment option. |
Create a backField with the following value:
Register your pass for additional benefits!
{psRegisterURL}
Set the attributedValue
to:
<a href="{psRegisterURL}">Register your pass</a> for additional value!
Add the condition: !registered
Create a backField with the following value:
Update your information:
{psRegisterURL}
Set the attributedValue
to:
<a href="{psRegisterURL}">Update your information</a>
Add the condition: registered
Create a backField with label "Latest News" and value set to your message.
Open the advanced options and note the API path. (example: structure_backFields_news_value
)
Set the changeMessage
in the advanced settings to "%@
" (without the quotes).
Create an additional field with key psPortalUpdatePaths
and value "structure_backFields_news_value
" (without the quotes).
Go to the template portal page and enter in the new message to send to passes.
The locations value can be set using the JSON format specified by Apple. For example, to set the locations to the Eiffel Tower, the Empire State building, and the Statue of Liberty, the JSON would be:
[
{
"longitude":48.8583701,
"latitude":2.2944813,
"relevantText":"Welcome to the Eiffel Tower!"
},
{
"longitude":40.7484444,
"latitude":-73.9878441,
"altitude":381,
"relevantText":"Welcome to the 102nd floor of the Empire State Building!"
},
{
"longitude":40.6892534,
"latitude":-74.0466891,
"relevantText":"Welcome to the Statue of Liberty!"
}
]
The JSON needs to be URL encoded to pass it as a parameter using the URL API. You can do this online using our encoding tool.
The encoded result is:
%5B%0A%09%7B%0A%09%09%22longetude%22%3A48.8583701%2C%0A%09%09%22latitude%22%3A2.2944813%2C%0A%09%09%22relevantText%22%3A%22Welcome%20to%20the%20Eiffel%20Tower!%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22longitude%22%3A40.7484444%2C%0A%09%09%22latitude%22%3A-73.9878441%2C%0A%09%09%22altitude%22%3A381%2C%0A%09%09%22relevantText%22%3A%22Welcome%20to%20the%20102nd%20floor%20of%20the%20Empire%20State%20Building!%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22longitude%22%3A40.6892534%2C%0A%09%09%22latitude%22%3A-74.0466891%2C%0A%09%09%22relevantText%22%3A%22Welcome%20to%20the%20Statue%20of%20Liberty!%22%0A%09%7D%0A%5D%0A
This link will create a demo pass with the locations automatically set:
https://www.passsource.com/pass/create.php?templateId=4&locations=%5B%0A%09%7B%0A%09%09%22longitude%22%3A48.8583701%2C%0A%09%09%22latitude%22%3A2.2944813%2C%0A%09%09%22relevantText%22%3A%22Welcome%20to%20the%20Eiffel%20Tower!%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22longitude%22%3A40.7484444%2C%0A%09%09%22latitude%22%3A-73.9878441%2C%0A%09%09%22altitude%22%3A381%2C%0A%09%09%22relevantText%22%3A%22Welcome%20to%20the%20102nd%20floor%20of%20the%20Empire%20State%20Building!%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22longitude%22%3A40.6892534%2C%0A%09%09%22latitude%22%3A-74.0466891%2C%0A%09%09%22relevantText%22%3A%22Welcome%20to%20the%20Statue%20of%20Liberty!%22%0A%09%7D%0A%5D%0A
Add an onScan
trigger:
{"action":"PSActionSetValue","targetPath":"backgroundColor","value":"blue"}
Go into your template and note the API path to the field (for our example, we'll use structure_headerFields_points_value
).
Add an onRegister
trigger (note we added a condition to make sure the points only get granted once):
{"action":"PSActionIncrementValue","targetPath":"structure_headerFields_points_value","amount":25,"PSConditions":"!registered"}
This sets this scan display values based on whether the pass has been scanned or not for use with our scanning feature.
Add an additional field named subtriggers
with the following value:
[
{
"action" : "PSActionSetValue",
"targetPath" : "psScanColor",
"value" : "{PSColorGreen}"
},
{
"action" : "PSActionSetValue",
"targetPath" : "psScanText",
"value" : "First Scan"
},
{
"action" : "PSActionSetValue",
"targetPath" : "psScanColor",
"value" : "{PSColorYellow}",
"PSConditions" : "scanned"
},
{
"action" : "PSActionSetValue",
"targetPath" : "psScanText",
"value" : "Already Scanned",
"PSConditions" : "scanned"
}
]
Set both the onCheck
and onScan
triggers to:
{
"action": "PSActionExecuteTriggers",
"targetPath": "subtriggers"
}
For a backfield with a value path of structure_backFields_myKey_value
, set the conditions of the entire field to:
{structure_backFields_myKey_value} !isEmpty
Assuming the points field path is structure_headerFields_points_value
, set the conditions of the field to:
{structure_headerFields_points_value} > 20
Assuming the field path structure_secondaryFields_name_value
, set the conditions of the field to:
{structure_secondaryFields_name_value} = "PassSource"
Set the conditions of the field to: scanned
Set the conditions of the field to: !registered
To have the pass show the name and points in lists and for the scanner and editor descriptions, create an additional field with key psPassDescription
and set it to:
{structure_secondaryFields_name_value} has {structure_headerFields_points_value} points
The newest version of our Pass Scanner app will automatically identify hashes within a URL, so you can simply set the barcode message to {psPassURL}
and the code can be scanned to get the pass as well as be used to update the pass.
QR Code Generator is a great site for generating QR code images that can be printed on physical cards. Use the templateHash to create a link to generate a new pass with each scan, or use the hashedSerialNumber to generate a code for a specific pass.
Go into your template and note the API path to the field (for our example, we'll use structure_headerFields_points_value
).
Add an onScan
trigger:
{"action":"PSActionIncrementValue","targetPath":"structure_headerFields_points_value","amount":1}
First you need the templateHash for the template and make sure the barcode message, altText, and member name are all user editable (click the (i) button next to the field and check the User Editable box.)
For our example, we're setting the barcode message and altText to "9991212", and the member name (which is a primary field) to "George P. Burdell".
Don't forget to URL encode the values. You could use this method to distribute customized passes to a mailing list with each person's email containing a link to a pass with their name and member ID pre-filled so all they have to do is tap the link and tap add to get it in their Apple Wallet. You could add additional user-editable fields and direct to the registration page instead of the create page if you wanted to capture additional information.
Construct the link as follows:
https://www.passsource.com/create.php?templateHash=eNortjIysVIqLA00jIiyyC0pMXKOcPYqyrIozC4ItLVVsgZcMKPxCfk,&barcode_message=9991212&barcode_altText=9991212&structure_primaryFields_member_value=George%20P.%20Burdell
Go to the following link:
https://www.passsource.com/api/login.php?email={email}&password={password}
then extract the clientHash parameter from the returned JSON.
Create a coupon card (for our example, we're using our PassSource loyalty demo), then set up the following triggers to set up the automatic actions during a scan:
{"action":"PSActionSetValue","targetPath":"psScanText","value":"one punch added","PSConditions":"{type} = \"storeCard\""}
{"action":"PSActionSetValue","targetPath":"psScanText","value":"Reward redeemed!","PSConditions":"{type} = \"coupon\""}
{"action":"PSActionSetValue","targetPath":"psScanColor","value":"{PSColorGreen}"}
{"action":"PSActionIncrementValue","targetPath":"structure_headerFields_punches_value","amount":1,"PSConditions":"{type} = \"storeCard\""}
{"action":"PSActionIncrementValue","targetPath":"structure_headerFields_punches_value","amount":-10,"PSConditions":"{type} = \"coupon\""}
{"action":"PSActionSetValue","targetPath":"type","value":"storeCard","PSConditions":"{type} = \"coupon\""}
{"action":"PSActionSetValue","targetPath":"type","value":"coupon","PSConditions":"{structure_headerFields_punches_value} > 9"}
You can further enhance the experience by having different fields appear depending on if the pass is a coupon or a storeCard, so that you can indicate what will happen when it is scanned or change out the offer.
This widget will create a generic pass with the user-entered member number as the barcode message:
<form method="get" action="https://www.passsource.com/pass/create.php">
<input type="hidden" name="templateHash" value="eNortjIysVIqLA00jIiyyC0pMXKOcPYqyrIozC4ItLVVsgZcMKPxCfk," />
<label>Enter your member number:</label>
<input type="text" name="barcode_message" />
<input type="submit" value="Get Pass" />
</form>
This is the wrapper for CURL we use to make it easier to fetch the contents of a web page (you may already have something similar already built-in to your frameworks):
function fetchURL($url, $post_data = null, $cookie_data = null) {
$curl_connection = curl_init($url);
// First we set the connection timeout to 30 seconds, so we don't have our script waiting indefinitely if the remote server fails to respond.
curl_setopt($curl_connection, CURLOPT_CONNECTTIMEOUT, 20);
// CURLOPT_RETURNTRANSFER set to true forces cURL not to display the output of the request, but return it as a string.
curl_setopt($curl_connection, CURLOPT_RETURNTRANSFER, true);
// Finally, we set CURLOPT_FOLLOWLOCATION to 1 to instruct cURL to follow "Location: " redirects found in the headers sent by the remote site.
curl_setopt($curl_connection, CURLOPT_FOLLOWLOCATION, 1);
// save cookies
$cookie_path = tempnam("/tmp", "temp_cookie_" . time());
// echo "COOKIE PATH: $cookie_path";
if (!is_null($cookie_data)) {
file_put_contents($cookie_path, $cookie_data);
}
curl_setopt($curl_connection, CURLOPT_COOKIEFILE, $cookie_path);
curl_setopt($curl_connection, CURLOPT_COOKIEJAR, $cookie_path);
if (!empty($post_data) && is_array($post_data)) {
// Now we must prepare the data that we want to post. We can first store this in an array, with the key of an element being the same as the input name of a regular form, and the value being the value that we want to post for that field.
// In order to format the data like this, we are going to create strings for each key-value pair (for example key1=value1) then combine them in one string.
// PHP 5+ has a nice function for this: http_build_query
$post_string = http_build_query($post_data);
// Next, we need to tell cURL which string we want to post. For this, we use the CURLOPT_POSTFIELDS option.
curl_setopt($curl_connection, CURLOPT_POSTFIELDS, $post_string);
}
//perform our request
$result = curl_exec($curl_connection);
$headers = curl_getinfo($curl_connection);
//close the connection
curl_close($curl_connection);
// store cookie data into database
$cookie_data = file_get_contents($cookie_path);
// cleanup and remove the temp file
unlink($cookie_path);
return array($result, $cookie_data);
}
And here is an example of a wrapper we have for one of our API functions:
// pass updates and images can be fetched with just the template hash without the need for the client hash (if only modifying user editable fields)
function createPassFromTemplateFields($clientHash, $templateHash, $userFields) {
list($results, $_SESSION['psCookieData']) = fetchURL("https://www.passsource.com/api/createPassFromTemplateFields.php?templateHash=$templateHash&clientHash=$clientHash", array("JSON" => json_encode($userFields)), $_SESSION['psCookieData']);
return json_decode($results, true);
}
This creates a pass setting the barcode message and altText and member name.
#import <PassKit/PassKit.h>
#define template_hash @"eNortjIysVIqLA00jIiyyC0pMXKOcPYqyrIozC4ItLVVsgZcMKPxCfk,"
// you'll want to include your client hash if you're not just modifying user editable fields.
// do the following in a UIViewController
// run asynchronously to not block main thread
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// create pass with template
NSString *passURL = [NSString stringWithFormat:@"https://www.passsource.com/pass/create.php?templateHash=%@&barcode_message=9991212&barcode_altText=9991212&generic_primaryFields_member_value=George%%20P.%%20Burdell&download=true", template_hash];
NSLog(@"Loading pass from URL: %@", passURL);
NSData *passData = [NSData dataWithContentsOfURL:[NSURL URLWithString:passURL]];
PKPass *pass = [[PKPass alloc] initWithData:passData error:&error];
if (pass == nil) {
NSLog(@"Error creating pass: %@", error);
} else {
dispatch_async( dispatch_get_main_queue(), ^{
PKAddPassesViewController *apvc = [[PKAddPassesViewController alloc] initWithPass:pass];
[self presentViewController:apvc animated:YES completion:nil];
});
}
} else {
NSLog(@"There was a login error: %@", error);
}
});