Google Wallet Integration for Access Passes in Mobile Apps
An access pass in Google Wallet is a GenericObject on top of GenericClass. Generic is the universal type for anything that isn't a ticket, boarding pass, or coupon: parking pass, membership card, loyalty card, subscription, work badge. In the API, it's the most flexible type: field structure is defined arbitrarily via textModulesData, linksModuleData, and imageModulesData.
GenericObject Structure
def create_access_pass(class_id: str, holder_name: str,
pass_id: str, access_level: str) -> dict:
issuer_id = "YOUR_ISSUER_ID"
return {
"id": f"{issuer_id}.pass_{pass_id}",
"classId": class_id,
"state": "ACTIVE",
"cardTitle": {
"defaultValue": {"language": "en", "value": "Employee Pass"}
},
"subheader": {
"defaultValue": {"language": "en", "value": "Access Level"}
},
"header": {
"defaultValue": {"language": "en", "value": access_level} # "A1 — all zones"
},
"textModulesData": [
{
"header": "Holder",
"body": holder_name,
"id": "holder"
},
{
"header": "Pass ID",
"body": pass_id,
"id": "pass_id"
},
{
"header": "Valid Until",
"body": "12/31/2024",
"id": "valid_until"
}
],
"barcode": {
"type": "QR_CODE",
"value": pass_id,
"alternateText": pass_id[:8].upper()
},
"heroImage": {
"sourceUri": {
"uri": "https://yourcompany.com/office-photo.jpg"
}
},
"validTimeInterval": {
"start": {"date": "2024-01-01T00:00:00Z"},
"end": {"date": "2024-12-31T23:59:59Z"}
},
"notifications": {
"expiryNotification": {
"enableNotification": True
}
}
}
notifications.expiryNotification — Google Wallet automatically notifies the user 3 days before expiration. No additional server-side logic is required.
GenericClass: Minimal Configuration
def create_generic_class(class_suffix: str) -> dict:
issuer_id = "YOUR_ISSUER_ID"
return {
"id": f"{issuer_id}.{class_suffix}",
"issuerName": "Your Company",
"reviewStatus": "UNDER_REVIEW",
"enableSmartTap": False, # True only if you have NFC readers
"multipleDevicesAndHoldersAllowedStatus": "ONE_USER_ONE_DEVICE"
}
ONE_USER_ONE_DEVICE — the pass cannot be transferred to another user or saved on multiple devices of one account. For corporate passes, this is the right restriction.
Creating Class via REST API Before First JWT
The class must be created once via API — you can only embed the object in JWT, not create the class via JWT:
from googleapiclient.discovery import build
from google.oauth2 import service_account
def ensure_generic_class_exists(class_data: dict) -> bool:
creds = service_account.Credentials.from_service_account_file(
'service-account.json',
scopes=['https://www.googleapis.com/auth/wallet_object.issuer']
)
service = build('walletobjects', 'v1', credentials=creds)
try:
service.genericclass().get(resourceId=class_data["id"]).execute()
return True # already exists
except HttpError as e:
if e.resp.status == 404:
service.genericclass().insert(body=class_data).execute()
return True
return False
Android: Complete Flow
class PassActivity : AppCompatActivity() {
private val walletClient by lazy {
Wallet.getWalletClient(this, WalletOptions.Builder()
.setEnvironment(WalletConstants.ENVIRONMENT_PRODUCTION)
.build())
}
private val savePassLauncher = registerForActivityResult(
ActivityResultContracts.StartIntentSenderForResult()
) { result ->
when (result.resultCode) {
RESULT_OK -> {
analytics.track("wallet_pass_added")
binding.addToWalletBtn.text = "Already in Wallet"
binding.addToWalletBtn.isEnabled = false
}
RESULT_CANCELED -> Unit
}
}
fun onAddToWalletClicked() {
viewModel.generatePassJwt().observe(this) { jwt ->
walletClient.savePassesViaIntent(
SavePassesRequest.newBuilder().setJwt(jwt).build()
) { result ->
result.intentSender?.let {
savePassLauncher.launch(IntentSenderRequest.Builder(it).build())
} ?: run {
// Already added
binding.addToWalletBtn.isEnabled = false
}
}
}
}
}
Updating Pass (Access Level Change)
def update_pass_access_level(object_id: str, new_level: str):
service = build('walletobjects', 'v1', credentials=creds)
service.genericobject().patch(
resourceId=object_id,
body={
"header": {
"defaultValue": {"language": "en", "value": new_level}
}
}
).execute()
Changes appear in the user's Wallet within minutes — without reissuing the pass.
Timeline
1–3 days. Basic pass with QR code and auto-expiration notification — 1.5 days. Updates via API + device transfer restrictions — 2–3 days. Pricing is calculated individually.







