Webhook Payload

PurchaseKit normalizes Apple and Google billing notifications into a unified webhook format.

When your app receives a billing notification from Apple or Google, PurchaseKit processes it and forwards a normalized payload to your configured webhook URL.

Payload structure

{
  "event_id": "unique-event-identifier",
  "type": "subscription.created",
  "customer_id": "your-customer-id",
  "subscription_id": "store-subscription-id",
  "store": "apple",
  "store_product_id": "com.example.premium_monthly",
  "subscription_name": "default",
  "status": "active",
  "current_period_start": "2025-01-23T12:00:00Z",
  "current_period_end": "2025-02-23T12:00:00Z",
  "success_path": "/welcome"
}

Fields

Field Type Description
event_id string Unique identifier for this event. Apple: notification UUID. Google: message ID.
type string Normalized event type. See Event types below.
customer_id string Your customer identifier, passed when creating the purchase intent.
subscription_id string Store's subscription identifier. Stable across renewals.
store string Either "apple" or "google".
store_product_id string The product ID as configured in App Store Connect or Google Play Console.
subscription_name string Always "default" (reserved for future multi-subscription support).
status string Current subscription status: "active", "canceled", or "expired".
current_period_start string ISO 8601 timestamp of the current billing period start.
current_period_end string ISO 8601 timestamp when the current period ends. null if unknown.
success_path string The redirect path passed when creating the purchase intent. null if not set.

Event types

PurchaseKit normalizes store-specific notification types into four unified event types:

Event type Description Status
subscription.created New subscription purchased active
subscription.updated Subscription renewed, recovered, or modified active
subscription.canceled Auto-renewal turned off (still active until period end) canceled
subscription.expired Subscription ended, revoked, or refunded expired

Webhook signature

The purchasekit gem handles verification automatically when you mount the engine.

Each webhook request includes a signature header for verification:

X-PurchaseKit-Signature: sha256=abc123...

Verify the signature by computing HMAC-SHA256 of the raw request body using your webhook secret:

expected = "sha256=" + OpenSSL::HMAC.hexdigest(
  "SHA256",
  webhook_secret,
  request.raw_post
)
valid = ActiveSupport::SecurityUtils.secure_compare(expected, request.headers["X-PurchaseKit-Signature"])

Retry behavior

If your webhook endpoint returns a non-2xx response, PurchaseKit retries with exponential backoff:

Attempt Delay
1 5 seconds
2 5 minutes
3 1 hour
4 1 day
5 3 days

After 5 failed attempts, the delivery is marked as failed and the account owner receives an email notification.

Environments

Webhooks are delivered to different URLs based on environment:

Environment URL used
production Webhook URL
sandbox Sandbox Webhook URL (falls back to Webhook URL if not set)
xcode Sandbox Webhook URL

Configure both URLs in your app's webhook settings to test sandbox purchases separately.

Handling webhooks

The gem automatically verifies signatures and processes webhooks:

# config/routes.rb
mount PurchaseKit::Engine, at: "/purchasekit"

Subscribe to events in your app:

# config/initializers/purchasekit.rb
PurchaseKit.configure do |config|
  config.on_subscription_created do |payload|
    user = User.find_by(id: payload["customer_id"])
    user&.activate_subscription!
  end

  config.on_subscription_canceled do |payload|
    user = User.find_by(id: payload["customer_id"])
    user&.mark_subscription_canceled!
  end

  config.on_subscription_expired do |payload|
    user = User.find_by(id: payload["customer_id"])
    user&.expire_subscription!
  end
end

Next steps