17 min read

Webhook

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

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 200 for 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"
}

See class Webhook.Test

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 reason field 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 the purchases collection included in each webhook. See Server-Side Integration for the recommended approach.

Webhook Timing

Different platforms send webhooks at different stages:

  1. Google Play:

    • Webhooks are sent only after purchase acknowledgment
    • Unacknowledged purchases won't trigger webhooks
    • Purchases must be verified and acknowledged within 3 days
  2. 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.id to 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 cancelationReason is 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:

  1. 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
  2. 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

  1. 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
  2. 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:

  1. Check webhook logs for non-200 responses
  2. Set up alerts for webhook processing errors
  3. Regularly test all webhook endpoints
  4. 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:

  1. Fix the endpoint to return 200 status for all webhook types
  2. Use the "Test Webhook" feature in the dashboard to force a call
  3. 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.