Are you an iOS developer looking for a simple way to monitor your app's in-app purchases and subscriptions? In this guide, I'll show you how to set up a notification system that sends real-time App Store purchase events directly to your Discord, Slack and Telegram channels. Perfect for indie developers and small teams!
π Get Started Quickly: Check out the GitHub repository for ready-to-deploy code and detailed setup instructions.
The repository includes:
- Serverless function code for both Google Cloud Functions and Cloudflare Workers
- Configuration templates for Discord, Telegram, and Slack
- Example notification payloads and testing scripts
- Comprehensive documentation and troubleshooting guides
Table of Contents π
- What We'll Build π οΈ
- Setting Up Your Notification Channel π’
- Setting Up and Deploying to Google Cloud Function βοΈ
- Setting Up and Deploying to Cloudflare Workers β‘οΈ
- Connecting to App Store Connect π
- What You'll Get π±
- Advanced Features π§
- Troubleshooting π
- Platform Comparison π€
- Cost Considerations π°
- Security Best Practices π
- Technical Details and Examples π§
- Testing with cURL Examples π§ͺ
- What's Next π
- Need Help π€
What We'll Build π οΈ
We'll create a serverless function that:
- Receives App Store Server-to-Server notifications
- Processes the purchase events
- Sends beautifully formatted notifications to your preferred platform
Setting Up Your Notification Channel π’
For Discord:
1. Open your Discord server
Launch Discord and open the server where you want to create the webhook.
2. Create a new Channel
If you don't have a channel already, create one by clicking on the "+" sign next to your serverβs channel list.
3. Access Channel Settings
Hover over the channel you want to create a webhook for. Click the gear icon (βοΈ) to open Channel Settings.
4. Go to Integrations
In the left sidebar, scroll down and click on Integrations. Under the "Integrations" section, click on Create Webhook.
5. Configure Your Webhook
Name your webhook and customize its settings if needed. Afterward, click Copy Webhook URL to save the URL for later use.
For Telegram:
1. Message @BotFather on Telegram
Open Telegram and search for @botfather. This is the official bot used to create and manage bots on Telegram.
2. Create a new bot with /newbot
Start a chat with @botfather and type /newbot to begin the process of creating a new bot.
3. Copy your bot token
After creating your bot, youβll receive a token. Copy the token by clicking on the red highlighted area in the message from BotFather.
4. Create a channel and add your bot as an admin
If you donβt already have a Telegram channel, create one by going to the Telegram app and selecting New Channel. Once the channel is created, add your bot as an admin so it can send and receive messages.
5. Get your channel ID
Send a message to your channel first. Then forward that message to
@userinfobot
Write userinfobot in search, select the bot and forward the message.
Check message that UserInfoBot's sent you. (Eg. Your chat id: "-1002328487593")
Setting Up and Deploying to Google Cloud Function βοΈ
- Visit Google Cloud Console
- Create a new project or select an existing one
- Go to Cloud Run Functions
- Click "Create Function"
If you did not enable required APIs previously, you will see this modal. You should click Enable button. After enabled APIs, you will be redirected the creating new function page.
Enter a name (1), choose HTTPS (2), select Allow unauthenticated invocations (3)
- After you created function you will see the Code Editor
- You will have 2 files:
index.js
andpackage.json
- Copy the following code into
package.json
:
{
"name": "iap-notifications",
"version": "1.0.0",
"description": "App Store server-to-server (S2S) notifications handler for in-app purchases",
"main": "index.js",
"dependencies": {
"@google-cloud/functions-framework": "^3.4.4",
"axios": "^1.7.9",
"base64url": "^3.0.1",
"country-iso-3-to-2": "^1.1.1",
"date-fns-tz": "^3.2.0",
"jsonwebtoken": "^9.0.2"
},
"scripts": {
"start": "functions-framework --target=main --port=8080",
"deploy:gcf": "gcloud functions deploy iap-notifications --gen2 --runtime=nodejs20 --region=us-central1 --memory=256MB --timeout=60s --min-instances=0 --max-instances=10 --trigger-http --allow-unauthenticated",
"deploy:cf": "wrangler deploy"
},
"keywords": [
"app-store",
"server-to-server",
"notifications",
"telegram-bot",
"discord-webhook",
"slack-webhook"
],
"author": "Kadir Melih Can",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
}
- Copy the source code into
index.js
and configure your notification settings:
const CONFIG = {
general: {
allowSandboxNotifications: true,
timezone: "Europe/Istanbul",
dateFormat: "dd.MM.yyyy HH:mm:ss"
},
telegram: {
enabled: true,
botToken: "YOUR_BOT_TOKEN",
chatId: "YOUR_CHAT_ID"
},
discord: {
enabled: true,
webhookUrl: "YOUR_DISCORD_WEBHOOK_URL"
},
slack: {
enabled: true,
webhookUrl: "YOUR_SLACK_WEBHOOK_URL",
channel: "#app-store-notifications"
}
};
Do not forget to change Entry point field's value as main as in screenshot.
- Click "Deploy" and wait for the deployment to complete
- Copy your function's URL from the "Trigger" tab
Setting Up and Deploying to Cloudflare Workers β‘οΈ
1. Create a Cloudflare Account:
- Visit Cloudflare Dashboard
- Sign up for a free account if you don't have one
- Verify your email address
2. Create a New Worker:
- Click on "Workers & Pages" in the left sidebar
- Click "Create" button
- Select "Create Worker"
- Choose a name for your worker and deploy (e.g., "iap-notifications")
- After deployed successfully click the "Edit Code" button
- Paste code in
worker.js
file into the code editor.
3. Save and Deploy:
- Click "Save and deploy"
- Wait for deployment to complete
- Copy your worker's URL (format:
https://iap-notifications.your-username.workers.dev
)
Connecting to App Store Connect π
Using Adapty:
- Log in to Adapty
- Navigate to App settings > Apps in the Adapty dashboard
- Click to the iOS SDK section, scroll to App Store server notifications
- Enter your the Cloud Function or CloudFlare Worker's URL to URL for forwarding raw Apple events
- Click Save in the bottom left corner.
Using RevenueCat:
- Log in to RevenueCat
- Navigate to your iOS app under Project settings > Apps in the RevenueCat dashboard
- Scroll to the Apple Server to Server notification settings section, and enter your the Cloud Function or CloudFlare Worker URL in Apple Server Notification Forwarding URL
- Click Save Changes in the top right corner.
Direct App Store Connect Setup:
If you are using an internal solution, your own backend url is probably added here. Therefore, you can prepare an endpoint for your own backend and give it to the Google Cloud function or CloudFlare worker we prepared as the raw event arrives.
- Open App Store Connect
- Select your app
- Go to App Information
- Scroll to "App Store Server Notifications"
- Enter your Cloud Function or CloudFlare URL
- Save changes
What You'll Get π±
Once set up, you'll receive beautifully formatted notifications for events like:
- New purchases
- Subscription renewals
- Refunds
- Consumption requests
- Billing issues
Example Notifications by Platform
Telegram Notification
MyApp.bundle.id
π DID_RENEW $99.99 π΅
π Event: AUTO_RENEW_ENABLED
π· Product: premium_yearly
π Country: πΊπΈ USA
π° Price: 99.99 USD
βΉοΈ Additional Info:
β’ Environment: Production
β’ ID: 2000000000000000
β’ Type: Auto-Renewable Subscription
β’ Expires Date: 15.03.2025 14:30:00
Discord Notification
Discord notifications use rich embeds with:
- Color-coded status
- App Store icon thumbnail
- Clickable app link
- Organized fields for event details
- Footer with transaction details
Advanced Features π§
Customizing Notifications
You can customize the notification format by modifying the respective service classes in index.js
:
-
TelegramNotification
for Telegram -
DiscordNotification
for Discord -
SlackNotification
for Slack
Error Handling
The system includes:
- Automatic retries for failed API calls
- Rate limiting protection
- Sandbox environment filtering
- Detailed error logging
Timezone Support
Configure your preferred timezone in the CONFIG object:
general: {
timezone: "Your/Timezone",
dateFormat: "dd.MM.yyyy HH:mm:ss"
}
Common Timezone Examples:
// United States
timezone: "America/New_York" // Eastern Time
timezone: "America/Chicago" // Central Time
timezone: "America/Denver" // Mountain Time
timezone: "America/Los_Angeles" // Pacific Time
// Europe
timezone: "Europe/London" // British Time
timezone: "Europe/Paris" // Central European Time
timezone: "Europe/Istanbul" // Turkish Time
// Asia
timezone: "Asia/Dubai" // Gulf Time
timezone: "Asia/Singapore" // Singapore Time
timezone: "Asia/Tokyo" // Japan Time
// Australia
timezone: "Australia/Sydney" // Australian Eastern Time
Date Format Examples:
// Common Formats
dateFormat: "dd.MM.yyyy HH:mm:ss" // 31.12.2024 14:30:00
dateFormat: "MM/dd/yyyy hh:mm:ss a" // 12/31/2024 02:30:00 PM
dateFormat: "yyyy-MM-dd HH:mm:ss" // 2024-12-31 14:30:00
dateFormat: "dd MMM yyyy HH:mm" // 31 Dec 2024 14:30
// Format Tokens
// dd - Day of month (01-31)
// MM - Month (01-12)
// yyyy - Full year
// HH - Hours in 24h format (00-23)
// hh - Hours in 12h format (01-12)
// mm - Minutes (00-59)
// ss - Seconds (00-59)
// a - AM/PM marker
Troubleshooting π
-
No notifications received:
- Check if your webhook URLs are correct
- Verify your Cloud Function is deployed and accessible
- Ensure the correct URL is set in App Store Connect
-
Webhook errors:
- Verify your bot/webhook permissions
- Check if the channel/chat IDs are correct
- Ensure your notification service is enabled in CONFIG
-
Invalid timestamps:
- Update the timezone in CONFIG to match your location
- Verify the dateFormat string is correct
Comparison of Google Cloud Functions vs Cloudflare Workers: Free vs Paid Tiers π€
Which One Should You Choose?
Choose Google Cloud Functions if you:
- Need more free tier requests
- Want deeper integration with other Google services
- Require more computing resources
- Need detailed logging and monitoring
Choose Cloudflare Workers if you:
- Want global edge deployment
- Need better cold start performance
- Prefer simpler deployment process
- Want built-in DDoS protection
Both platforms are excellent choices for this project. For most indie developers, either option will work great as both offer generous free tiers and reliable performance.
Cost Considerations π°
Google Cloud Functions Free Tier
- 2 million invocations per month
- 400,000 GB-seconds of compute time
- 200,000 GHz-seconds of compute time
- 5GB network egress per month
- No credit card required to start
Paid Tier (if you exceed free tier):
- $0.40 per million invocations
- $0.0000025 per GB-second
- $0.0000100 per GHz-second
Cloudflare Workers Free Tier
- 100,000 requests per day (~3 million/month)
- Unlimited scripts/workers
- 128MB memory limit per worker
- 10ms CPU time per request
- Workers KV: 100,000 reads per day
- Global edge deployment included
Paid Tier (Workers Paid Plan):
- $5/month for 10 million requests
- Increased CPU limits (50ms)
- 1GB memory limit per worker
- Workers KV: 1 million reads per day
Cost Estimation Examples
For a typical indie app (5,000 monthly active users):
- Average 1-2 events per user per month
- Total: ~10,000 events/month
- Easily covered by both platforms' free tiers
For a growing app (50,000 monthly active users):
- Average 1-2 events per user per month
- Total: ~100,000 events/month
- Google Cloud Functions: Still within free tier
- Cloudflare Workers: May need paid plan depending on daily distribution
For a large app (500,000 monthly active users):
- Average 1-2 events per user per month
- Total: ~1,000,000 events/month
- Google Cloud Functions:
- Still within free tier
- Minimal cost if exceeded (~$0.40)
- Cloudflare Workers:
- Paid plan recommended ($5/month)
- Better global performance
Cost Optimization Tips
-
Monitor Usage
- Set up billing alerts
- Track daily/monthly usage
- Monitor error rates
-
Reduce Costs
- Filter unnecessary notifications
- Batch events when possible
- Optimize code execution time
-
Choose the Right Plan
- Start with free tier
- Upgrade only when needed
- Consider your app's growth rate
Both platforms offer excellent value for money, especially for indie developers and small teams. The free tiers are typically sufficient for apps with up to 100,000 monthly active users, making them perfect for getting started and scaling up.
Security Best Practices π
- Keep your webhook URLs private
- Enable Cloud Function authentication if needed
- Regularly rotate webhook URLs
- Monitor function logs for unusual activity
- Set up billing alerts in Google Cloud
Technical Details and Examples π§
Example App Store Notification
Here's an example of the raw JSON data you'll receive from Apple (with randomized sensitive information):
{
"data": {
"appAppleId": 9876543210,
"bundleId": "com.example.myapp",
"bundleVersion": "4",
"environment": "Production",
"renewalInfo": {
"autoRenewProductId": "myapp_premium_annual",
"autoRenewStatus": 1,
"currency": "USD",
"environment": "Production",
"originalTransactionId": "200001234567890",
"productId": "myapp_premium_annual",
"recentSubscriptionStartDate": 1729488392000,
"renewalDate": 1761283592000,
"renewalPrice": 99990,
"signedDate": 1735414243588
},
"transactionInfo": {
"bundleId": "com.example.myapp",
"currency": "USD",
"environment": "Production",
"expiresDate": 1761283592000,
"inAppOwnershipType": "PURCHASED",
"originalTransactionId": "200001234567890",
"price": 99990,
"productId": "myapp_premium_annual",
"purchaseDate": 1729747592000,
"storefront": "USA",
"storefrontId": "143441",
"transactionId": "200009876543210",
"type": "Auto-Renewable Subscription"
}
},
"notificationType": "DID_CHANGE_RENEWAL_STATUS",
"subtype": "AUTO_RENEW_ENABLED",
"version": "2.0"
}
Understanding the Notification Fields
Key fields in the notification:
-
Top Level
-
notificationType
: The main event type (e.g., DID_CHANGE_RENEWAL_STATUS) -
subtype
: Specific event subtype (e.g., AUTO_RENEW_ENABLED) -
version
: API version (currently 2.0)
-
-
Renewal Info
-
autoRenewProductId
: Product identifier for the subscription -
autoRenewStatus
: Current auto-renewal status (1 = enabled, 0 = disabled) -
renewalDate
: Next renewal date in milliseconds -
renewalPrice
: Price in minor units (e.g., 99990 = $99.99)
-
-
Transaction Info
-
inAppOwnershipType
: Purchase type (PURCHASED, FAMILY_SHARED, etc.) -
originalTransactionId
: Original purchase identifier -
transactionId
: Current transaction identifier -
purchaseDate
: Purchase timestamp in milliseconds -
expiresDate
: Subscription expiration timestamp
-
Processing Best Practices
-
Validation
- Always verify the notification's signature
- Check environment (Production vs. Sandbox)
- Validate bundle ID matches your app
-
Error Handling
- Implement exponential backoff for retries
- Log failed notification processing
- Set up alerts for repeated failures
-
Data Storage
- Store raw notifications for debugging
- Index by transactionId and originalTransactionId
- Keep audit logs of processed notifications
cURL Examples App Store Notification
Here's an example of the raw JSON data you'll receive from Apple (with randomized sensitive information):
{
"signedPayload": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImFwcEFwcGxlSWQiOjEyMzQ1Njc4OTAsImJ1bmRsZUlkIjoiY29tLmx1bmFyLmZpdG5lc3Nwcm8iLCJidW5kbGVWZXJzaW9uIjoiMS4wIiwiZW52aXJvbm1lbnQiOiJQcm9kdWN0aW9uIiwidHJhbnNhY3Rpb25JbmZvIjp7ImJ1bmRsZUlkIjoiY29tLmx1bmFyLmZpdG5lc3Nwcm8iLCJjdXJyZW5jeSI6IktSVyIsImVudmlyb25tZW50IjoiUHJvZHVjdGlvbiIsImV4cGlyZXNEYXRlIjoxNzk5OTk5OTk5MDAwLCJpbkFwcE93bmVyc2hpcFR5cGUiOiJQVVJDSEFTRUQiLCJvcmlnaW5hbFRyYW5zYWN0aW9uSWQiOiI0ODAwMDIwNTc0ODk0ODMiLCJwcmljZSI6MjkwMDAsInByb2R1Y3RJZCI6ImZpdG5lc3Nwcm9fcHJlbWl1bV95ZWFybHkiLCJwdXJjaGFzZURhdGUiOjE3MDM4MDY1MjAwMDAsInN0b3JlZnJvbnQiOiJLT1IiLCJ0eXBlIjoiQXV0by1SZW5ld2FibGUgU3Vic2NyaXB0aW9uIn19LCJub3RpZmljYXRpb25UeXBlIjoiRElEX0NIQU5HRV9SRU5FV0FMX1BSRUYiLCJzdWJ0eXBlIjoiVVBHUkFERSIsInZlcnNpb24iOiIyLjAifQ.secret"
}
Testing with CURL Examples π§ͺ
Here are some example CURL commands to test your notification system with different scenarios. Replace YOUR_FUNCTION_URL
with your actual Cloud Function or Cloudflare Worker URL.
1. Subscription Renewal (Korean Market)
curl -X POST "YOUR_FUNCTION_URL" \
-H "Content-Type: application/json" \
-d '{
"signedPayload": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImFwcEFwcGxlSWQiOjEyMzQ1Njc4OTAsImJ1bmRsZUlkIjoiY29tLmx1bmFyLmZpdG5lc3Nwcm8iLCJidW5kbGVWZXJzaW9uIjoiMS4wIiwiZW52aXJvbm1lbnQiOiJQcm9kdWN0aW9uIiwidHJhbnNhY3Rpb25JbmZvIjp7ImJ1bmRsZUlkIjoiY29tLmx1bmFyLmZpdG5lc3Nwcm8iLCJjdXJyZW5jeSI6IktSVyIsImVudmlyb25tZW50IjoiUHJvZHVjdGlvbiIsImV4cGlyZXNEYXRlIjoxNzk5OTk5OTk5MDAwLCJpbkFwcE93bmVyc2hpcFR5cGUiOiJQVVJDSEFTRUQiLCJvcmlnaW5hbFRyYW5zYWN0aW9uSWQiOiI0ODAwMDIwNTc0ODk0ODMiLCJwcmljZSI6MjkwMDAsInByb2R1Y3RJZCI6ImZpdG5lc3Nwcm9fcHJlbWl1bV95ZWFybHkiLCJwdXJjaGFzZURhdGUiOjE3MDM4MDY1MjAwMDAsInN0b3JlZnJvbnQiOiJLT1IiLCJ0eXBlIjoiQXV0by1SZW5ld2FibGUgU3Vic2NyaXB0aW9uIn19LCJub3RpZmljYXRpb25UeXBlIjoiRElEX0NIQU5HRV9SRU5FV0FMX1BSRUYiLCJzdWJ0eXBlIjoiVVBHUkFERSIsInZlcnNpb24iOiIyLjAifQ.secret"
}'
2. Subscription Refund (German Market)
curl -X POST "YOUR_FUNCTION_URL" \
-H "Content-Type: application/json" \
-d '{
"signedPayload": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImFwcEFwcGxlSWQiOjEyMzQ1Njc4OTAsImJ1bmRsZUlkIjoiY29tLnplbml0aC5tZWRpdGF0ZSIsImJ1bmRsZVZlcnNpb24iOiIxLjAiLCJlbnZpcm9ubWVudCI6IlByb2R1Y3Rpb24iLCJ0cmFuc2FjdGlvbkluZm8iOnsiYnVuZGxlSWQiOiJjb20uemVuaXRoLm1lZGl0YXRlIiwiY3VycmVuY3kiOiJFVVIiLCJlbnZpcm9ubWVudCI6IlByb2R1Y3Rpb24iLCJleHBpcmVzRGF0ZSI6MTc5OTk5OTk5OTAwMCwiaW5BcHBPd25lcnNoaXBUeXBlIjoiUFVSQ0hBU0VEIiwib3JpZ2luYWxUcmFuc2FjdGlvbklkIjoiMzcwMDAxODU4MzM3NzcxIiwicHJpY2UiOjE0OTksInByb2R1Y3RJZCI6Im1lZGl0YXRlX3VubGltaXRlZF95ZWFybHkiLCJwdXJjaGFzZURhdGUiOjE3MDM4MjI2NTkwMDAsInN0b3JlZnJvbnQiOiJERVUiLCJ0eXBlIjoiQXV0by1SZW5ld2FibGUgU3Vic2NyaXB0aW9uIn19LCJub3RpZmljYXRpb25UeXBlIjoiUkVGVU5EIiwic3VidHlwZSI6bnVsbCwidmVyc2lvbiI6IjIuMCJ9.secret"
}'
3. Subscription Renewal (Brazilian Market)
curl -X POST "YOUR_FUNCTION_URL" \
-H "Content-Type: application/json" \
-d '{
"signedPayload": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImFwcEFwcGxlSWQiOjEyMzQ1Njc4OTAsImJ1bmRsZUlkIjoiY29tLnNreXdhdmUucGhvdG9tYWdpYyIsImJ1bmRsZVZlcnNpb24iOiIxLjAiLCJlbnZpcm9ubWVudCI6IlByb2R1Y3Rpb24iLCJ0cmFuc2FjdGlvbkluZm8iOnsiYnVuZGxlSWQiOiJjb20uc2t5d2F2ZS5waG90b21hZ2ljIiwiY3VycmVuY3kiOiJCUkwiLCJlbnZpcm9ubWVudCI6IlByb2R1Y3Rpb24iLCJleHBpcmVzRGF0ZSI6MTc5OTk5OTk5OTAwMCwiaW5BcHBPd25lcnNoaXBUeXBlIjoiUFVSQ0hBU0VEIiwib3JpZ2luYWxUcmFuc2FjdGlvbklkIjoiNDEwMDAyMjYzNTMxNjA2IiwicHJpY2UiOjY5OTAsInByb2R1Y3RJZCI6InBob3RvbWFnaWNfcHJvX2FubnVhbF90aWVyMSIsInB1cmNoYXNlRGF0ZSI6MTcwMzg3MjYwOTAwMCwic3RvcmVmcm9udCI6IkJSQSIsInR5cGUiOiJBdXRvLVJlbmV3YWJsZSBTdWJzY3JpcHRpb24ifX0sIm5vdGlmaWNhdGlvblR5cGUiOiJESURfUkVORVciLCJzdWJ0eXBlIjpudWxsLCJ2ZXJzaW9uIjoiMi4wIn0.secret"
}'
These examples demonstrate different notification types (renewal, refund, upgrade) across various markets with different currencies. The payloads are base64-encoded JWTs that will be decoded by the notification handler. You can use these to test your notification system's handling of:
-
Different Currencies & Regions
- Korean Won (KRW)
- Euro (EUR)
- Brazilian Real (BRL)
-
Various Notification Types
- DID_CHANGE_RENEWAL_PREF with UPGRADE subtype
- REFUND
- DID_RENEW
-
Price Formats
- Whole numbers (29 KRW)
- Decimal prices (6.99 BRL)
- Four-digit prices (14.99 EUR)
To use these examples:
- Replace
YOUR_FUNCTION_URL
with your actual endpoint URL - Run the CURL command in your terminal
- Check your configured notification channels (Discord/Telegram/Slack) for the formatted notification
What's Next π
Now that you have real-time purchase notifications set up, you can:
- Monitor your app's revenue in real-time
- Quickly respond to refund requests
- Track subscription patterns
- Identify potential issues early
- Make data-driven decisions for your app
Remember to star the repository if you found it helpful! βοΈ
Need Help π€
Acknowledgments π
Special thanks to Ramazan Arslan for his initial implementation that inspired this project. I started by using his published version and then further developed it to make it simpler and more accessible for everyone to implement their own App Store server notifications handler.
This project is built with these amazing open-source packages:
- @google-cloud/functions-framework - For serverless function development
- Axios - For HTTP requests
- base64url - For URL-safe base64 encoding
- country-iso-3-to-2 - For country code conversion
- jsonwebtoken - For JWT handling
- date-fns-tz - For timezone-aware date formatting
Happy coding! π
Top comments (1)
This is hands down the best guide Iβve come across for App Store in-app purchase events. π I followed it to set up notifications for Telegram, and it worked perfectly. π€ Excellent job! π