Google Wallet Integration for Coupons and Discounts in Mobile Apps
A coupon in Google Wallet is an OfferObject on top of OfferClass. Unlike Apple Wallet where coupons are just another .pkpass type, Google Wallet separates templates (class) and instances (object). One OfferClass describes a promotion, thousands of OfferObject are personalized coupons for specific users with individual barcodes.
OfferClass: Promotion Template
def create_offer_class(campaign_id: str, title: str, discount_value: str) -> dict:
issuer_id = "YOUR_ISSUER_ID"
return {
"id": f"{issuer_id}.offer_{campaign_id}",
"issuerName": "Your Store",
"title": title, # "15% discount on electronics"
"redemptionChannel": "BOTH", # ONLINE, INSTORE or BOTH
"provider": "Your Store",
"titleImage": {
"sourceUri": {"uri": "https://yourapp.com/offer-banner.jpg"},
"contentDescription": {
"defaultValue": {"language": "en", "value": title}
}
},
"details": f"Get {discount_value} off entire selection. Cannot be combined with other offers.",
"finePrint": "Valid until 12/31/2024. One-time use.",
"validTimeInterval": {
"start": {"date": "2024-06-01T00:00:00Z"},
"end": {"date": "2024-12-31T23:59:59Z"}
},
"hexBackgroundColor": "#1A73E8",
"reviewStatus": "UNDER_REVIEW"
}
OfferObject: Personalized Coupon
def create_offer_object(class_id: str, user_id: str, coupon_code: str) -> dict:
issuer_id = "YOUR_ISSUER_ID"
return {
"id": f"{issuer_id}.coupon_{user_id}_{coupon_code}",
"classId": class_id,
"state": "ACTIVE",
"barcode": {
"type": "CODE_128", # or QR_CODE, PDF_417
"value": coupon_code,
"alternateText": coupon_code
},
"validTimeInterval": {
"start": {"date": "2024-06-01T00:00:00Z"},
"end": {"date": "2024-12-31T23:59:59Z"}
},
"textModulesData": [
{
"header": "Your Promo Code",
"body": coupon_code,
"id": "coupon_code"
}
]
}
textModulesData displays in the lower part of the coupon — useful for a promo code that a cashier can manually enter as a backup if the scanner fails.
JWT with Coupon
def generate_offer_jwt(offer_object: dict) -> str:
payload = {
"iss": service_account_email,
"aud": "google",
"typ": "savetowallet",
"iat": int(time.time()),
"payload": {
"offerObjects": [offer_object]
}
}
return jwt.encode(payload, private_key, algorithm="RS256")
Android: Add Button
// Show button only if Wallet is available
walletClient.getPayApiAvailabilityStatus(
PayApiAvailabilityStatusRequest.newBuilder()
.setRequestType(PayApiAvailabilityStatusRequest.RequestType.SAVE_PASSES)
.build()
).addOnSuccessListener { status ->
binding.addCouponToWalletBtn.isVisible = status.isAvailable
}
// Add coupon
fun addCouponToWallet(jwt: String) {
walletClient.savePassesViaIntent(
SavePassesRequest.newBuilder().setJwt(jwt).build()
) { result ->
result.intentSender?.let { sender ->
addToWalletLauncher.launch(
IntentSenderRequest.Builder(sender).build()
)
}
}
}
Deactivating Coupon After Use
After the cashier scans the coupon, the backend should transition the object to EXPIRED:
def redeem_coupon(object_id: str):
service = build('walletobjects', 'v1', credentials=credentials)
service.offerobject().patch(
resourceId=object_id,
body={"state": "EXPIRED"}
).execute()
The pass in the user's Wallet automatically gets a "Used" badge without being deleted — important for purchase history.
Bulk Coupon Distribution
For distributing coupons to many users, you don't need to generate JWT for each individually when the button is clicked. You can pre-create OfferObject via REST API and store objectId in the database. The "Add to Wallet" button generates JWT with the already-created objectId — Google returns intentSender for the existing object.
Timeline
1–3 days. Promotion template + personalized coupons + deactivation — 2 days. Bulk pre-generation of objects — additionally 0.5 day. Pricing is calculated individually.







