A webhook is an endpoint used for server-to-server real-time notifications through HTTP.
Description
In order to keep your users purchases statuses up-to-date on your server, you can setup an URL that iaptic will call whenever there's an important event changing the state of that user.
This Webhook will be called when the user validates a receipt or when Apple, Google, Stripe (and other platforms) themselves sends a notification, for subscription events for instance.
The webhook URL is setup from the Settings page.
Requirements
- Connection with the Google Publisher API.
- Configure the Apple Webhook.
- Attach the application username when making purchases.
If any of those requirements are not met, your webhook endpoint will not be called.
Webhook Request
Iaptic will make a request to your Webhook URLs using the POST method. The body will be a JSON object containing the webhook type and the associated data.
The format is described here: class Webhook.PurchasesUpdated.
Sandbox Webhook URL
In the settings you will find the Webhook URL and Sandbox Webhook URL fields.
- If only the Webhook URL is set, all notifications will be sent to that URL.
- If the Sandbox Webhook URL is set, it'll receive notifications for users making "sandbox" purchases.
- the "Webhook URL" will then only receive notifications for users having made "production" purchases.
Multiple URLs
In each of those fields, you can set multiple URLs by separating them with a comma.
Important
- Your endpoint should use SSL, i.e. be an
https://endpoint. Self-signed certificates are accepted. - Make sure to return status
200for all calls, even for unrecognized webhook types. The webhook might be extended with new message types. - If your server responds with a status other than
200, iaptic will retry the webhook call. See Retry Strategy for more details.
Testing your Webhook
Next to the Webhook URL field, there's a Test button. Clicking this button will send a test webhook notification to ALL your Webhook URLs.
The content of the webhook will be as follows:
{
"type": "test",
"password": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
Notifications
Since version 3.3, webhook calls include a notification field. This field lets your server know why the call was made.
It can be used to build analytics, for instance in your code:
const notification = webhook.notification;
if (notification.reason != "RECEIPT_VALIDATED") { // storing those is rarely useful
notificationsDB.insert(
notification.id, notification.date, notification.reason,
notification.productId, notification.purchaseId);
}
// ...
// Count purchases since January 2023
notificationsDB.pseudoSQL('SELECT count(*) WHERE date > "2023-01-01" AND reason = "PURCHASED"');
// Count renewals since January 2023
notificationsDB.pseudoSQL('SELECT count(*) WHERE date > "2023-01-01" AND reason = "RENEWED"');
Check NotificationReason for the list of possible notification reasons.
Important: The
reasonfield is designed for analytics, logging, and UI feedback — not for driving business logic. Don't use it to determine entitlements (e.g. "if reason is PURCHASED, credit the account"). Instead, always derive user entitlements from thepurchasescollection included in each webhook. See Server-Side Integration for the recommended approach.
Webhook Timing
Different platforms send webhooks at different stages:
-
Google Play:
- Webhooks are sent only after purchase acknowledgment
- Unacknowledged purchases won't trigger webhooks
- Purchases must be verified and acknowledged within 3 days
-
Apple App Store:
- Initial purchase notifications are sent immediately
- Subscription status updates are sent in real-time
- Server-to-server notifications don't depend on acknowledgment
- If a notification arrives before the app validates its first receipt, iaptic queues it and automatically processes it after the first successful validation — no webhook is lost
Webhook Types
Webhook notifications carry a notification.reason field indicating what triggered the event. Below are all possible reasons, organized by category.
Subscription Purchase & Lifecycle
| Reason | Description |
|---|---|
| ACKNOWLEDGED | Purchase was acknowledged by the server |
| PURCHASED | A subscription purchase was validated and acknowledged. For Google Play, this only fires after explicit acknowledgment. For Apple, this fires after successful validation. Non-subscription purchases use ONE_TIME_PURCHASED instead |
| RENEWED | Subscription renewed successfully; includes the new expiry date |
| EXPIRED | Subscription expired; includes the expiry date |
| REVOKED | Subscription was revoked before expiration (e.g. by App Store support or leaving Family Sharing) |
Subscription Status Changes
| Reason | Description |
|---|---|
| WILL_LAPSE | Subscription is set to lapse at the end of the billing period — auto-renew was turned off |
| WILL_AUTO_RENEW | Subscription is set to auto-renew at the end of the billing period |
| PRICE_CHANGE_CONFIRMED | A subscription price change was confirmed by the user |
| PRICE_CHANGE_UPDATED | A subscription price change was updated (confirmed, declined, or pending) |
| EXTENDED | Subscription renewal date was extended (e.g. by App Store offer codes) |
| PLAN_CHANGED | Subscription plan was changed (upgrade or downgrade) |
| PAUSED | Subscription was paused (Google Play) |
| ENTERED_GRACE_PERIOD | Subscription entered the grace period (billing retry in progress) |
Refunds & Cancellations
| Reason | Description |
|---|---|
| REFUNDED | Purchase was refunded |
One-time Purchases
| Reason | Description |
|---|---|
| ONE_TIME_PURCHASED | A non-subscription product was purchased (one-time unlocks, consumables). Applies to Apple one-time charges, Google one-time products, and Stripe one-time payments. Kept separate from PURCHASED so subscription acquisition metrics aren't mixed with consumable volume |
| ONE_TIME_CANCELED | A non-subscription purchase was canceled or voided |
Receipt Events
| Reason | Description |
|---|---|
| RECEIPT_VALIDATED | Receipt was validated successfully; the purchase is valid but may not be acknowledged yet |
| RECEIPT_REFRESHED | Receipt data was refreshed (e.g. on re-verification) |
System Events
| Reason | Description |
|---|---|
| REPEATED | Webhook was manually repeated by a user via the Dashboard or REST API |
| OTHER | Other uncategorized event |
| TEST | Test webhook (sent via the Dashboard "Test" button) |
The full list is also available in the API reference.
Webhook Order
Webhooks may be received in any order. For example:
- A RECEIPT_VALIDATED webhook might arrive after PURCHASED
- Multiple webhooks might be sent for the same event
- Your server should handle duplicate notifications gracefully (use
notification.idto detect duplicates)
Cancelation Reasons
Transactions and purchases may include a cancelationReason field when they have been canceled or revoked. Possible values:
| Reason | Description |
|---|---|
| Developer | Canceled by the developer |
| System | Canceled by the store system for an unspecified reason |
| System.Replaced | Subscription upgraded or downgraded to a new plan |
| System.ProductUnavailable | Product was not available for purchase at the time of renewal |
| System.BillingError | Billing error at the store level (e.g. customer's payment information is no longer valid) |
| System.Deleted | Subscription is gone, generally because the user's account was deleted |
| Customer | Canceled by the customer for an unspecified reason |
| Customer.Cost | Customer found the price too high |
| Customer.FoundBetterApp | Customer found a better alternative |
| Customer.NotUsefulEnough | Customer didn't find the product useful enough |
| Customer.PriceIncrease | Customer did not agree to a recent price increase |
| Customer.TechnicalIssues | Customer canceled due to an actual or perceived issue within your app |
| Customer.OtherReason | Other voluntary cancellation reason (e.g. accidental purchase) |
| Unknown | Canceled for unknown reasons |
Note: A
cancelationReasonis only present when the purchase has actually been canceled. Active purchases do not include this field.
See also CancelationReason in the API reference.
Debugging Missing Webhooks
If you're not receiving expected webhooks:
-
For Google Play:
- Verify the purchase was acknowledged by the app
- Check the transaction ID in iaptic dashboard
- Review client-side logs for validation errors
-
For Apple App Store:
- Verify the receipt was validated
- Check server-to-server notification settings
- Review notification history in App Store Connect
Error Handling Best Practices
-
Always Return 200 Status
- Return HTTP 200 even for webhooks you don't process
- Return HTTP 200 even for validation errors (e.g., "Invalid user ID", "User not found", ...)
- Only return non-200 status for actual server errors
-
Webhook Processing
- Process what you can, ignore what you don't understand
- Log unrecognized webhook types for future compatibility
- Handle duplicate notifications gracefully
Monitoring Webhook Health
To ensure your webhooks are working properly:
- Check webhook logs for non-200 responses
- Set up alerts for webhook processing errors
- Regularly test all webhook endpoints
- Monitor webhook latency and success rates
Retry Strategy
When a webhook call fails (non-200 response), iaptic implements the following retry mechanism:
- Up to 7 retry attempts with exponential backoff (1h, 2h, 3h, etc.)
- Automatic safety net retry after 4-6 minutes
- Maximum 8 attempts within a 24-hour window
- After all retries fail, the webhook is marked as failed
- 100 successive failures will trigger blacklisting of your endpoint
Blacklisted URLs
Endpoints will be blacklisted if they consistently fail:
- 100 successive non-200 responses triggers blacklisting
- Blacklisting lasts for 30 days
- All webhook calls to blacklisted endpoints are suspended, except TEST webhooks
If you want to restore a blacklisted URL:
- Fix the endpoint to return 200 status for all webhook types
- Use the "Test Webhook" feature in the dashboard to force a call
- If the test succeeds, the endpoint will be immediately removed from the blacklist
Important: A blacklisted endpoint means you might miss important purchase updates. Always monitor your webhook endpoints' health and ensure they handle all webhook types correctly.
