Created
December 20, 2025 09:43
-
-
Save bryanmylee/95dd9be99659c71aa01654bce18f0209 to your computer and use it in GitHub Desktop.
Valibot schema for RevenueCat Webhook event
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import * as v from "valibot"; | |
| export const RCEventTypeSchema = v.enum({ | |
| TEST: "TEST", | |
| INITIAL_PURCHASE: "INITIAL_PURCHASE", | |
| RENEWAL: "RENEWAL", | |
| CANCELLATION: "CANCELLATION", | |
| UNCANCELLATION: "UNCANCELLATION", | |
| NON_RENEWING_PURCHASE: "NON_RENEWING_PURCHASE", | |
| SUBSCRIPTION_PAUSED: "SUBSCRIPTION_PAUSED", | |
| EXPIRATION: "EXPIRATION", | |
| BILLING_ISSUE: "BILLING_ISSUE", | |
| PRODUCT_CHANGE: "PRODUCT_CHANGE", | |
| TRANSFER: "TRANSFER", | |
| SUBSCRIPTION_EXTENDED: "SUBSCRIPTION_EXTENDED", | |
| TEMPORARY_ENTITLEMENT_GRANT: "TEMPORARY_ENTITLEMENT_GRANT", | |
| REFUND_REVERSED: "REFUND_REVERSED", | |
| INVOICE_ISSUANCE: "INVOICE_ISSUANCE", | |
| VIRTUAL_CURRENCY_TRANSACTION: "VIRTUAL_CURRENCY_TRANSACTION", | |
| EXPERIMENT_ENROLLMENT: "EXPERIMENT_ENROLLMENT", | |
| SUBSCRIBER_ALIAS: "SUBSCRIBER_ALIAS", | |
| }); | |
| export type RCEventType = v.InferInput<typeof RCEventTypeSchema>; | |
| /* | |
| * Helpers. | |
| */ | |
| const RCTimestampSchema = v.pipe(v.number(), v.integer()); | |
| const RCSubscriberAttributeSchema = v.object({ | |
| updated_at_ms: RCTimestampSchema, | |
| value: v.string(), | |
| }); | |
| const RCExperimentSchema = v.object({ | |
| experiment_id: v.string(), | |
| experiment_variant: v.string(), | |
| /** | |
| * Measured in milliseconds since Unix epoch | |
| */ | |
| enrolled_at_ms: RCTimestampSchema, | |
| }); | |
| const RCCommonEventFieldSchema = v.object({ | |
| id: v.string(), | |
| /** | |
| * Unique identifier of the app the event is associated with. Corresponds to | |
| * an app within a project. This value can be found in the app's | |
| * configuration page in project settings. | |
| */ | |
| app_id: v.string(), | |
| /** | |
| * The time that the event was generated. Does not necessarily coincide with | |
| * when the action that triggered the event occurred (purchased, cancelled, | |
| * etc). | |
| */ | |
| event_timestamp_ms: RCTimestampSchema, | |
| /** | |
| * Last seen app user id of the subscriber. | |
| */ | |
| app_user_id: v.string(), | |
| /** | |
| * The first app user id used by the subscriber. | |
| */ | |
| original_app_user_id: v.string(), | |
| /** | |
| * All app user ids ever used by the subscriber. | |
| */ | |
| aliases: v.array(v.string()), | |
| /** | |
| * Map of attribute names to attribute objects. | |
| */ | |
| subscriber_attributes: v.record(v.string(), RCSubscriberAttributeSchema), | |
| /** | |
| * The experiments that the subscriber was enrolled in, if any. | |
| */ | |
| experiments: v.array(RCExperimentSchema), | |
| }); | |
| const RCSubscriptionPeriodTypeSchema = v.enum({ | |
| /** | |
| * Free trials. | |
| */ | |
| TRIAL: "TRIAL", | |
| /** | |
| * Introductory pricing. | |
| */ | |
| INTRO: "INTRO", | |
| /** | |
| * Standard subscription. | |
| */ | |
| NORMAL: "NORMAL", | |
| /** | |
| * Subscription granted throgh RevenueCat. | |
| */ | |
| PROMOTIONAL: "PROMOTIONAL", | |
| /** | |
| * Play Store prepaid transactions. | |
| */ | |
| PREPAID: "PREPAID", | |
| }); | |
| const RCStoreSchema = v.enum({ | |
| AMAZON: "AMAZON", | |
| APP_STORE: "APP_STORE", | |
| MAC_APP_STORE: "MAC_APP_STORE", | |
| PADDLE: "PADDLE", | |
| PLAY_STORE: "PLAY_STORE", | |
| PROMOTIONAL: "PROMOTIONAL", | |
| RC_BILLING: "RC_BILLING", | |
| ROKU: "ROKU", | |
| STRIPE: "STRIPE", | |
| TEST_STORE: "TEST_STORE", | |
| }); | |
| const RCEnvironmentSchema = v.enum({ | |
| SANDBOX: "SANDBOX", | |
| PRODUCTION: "PRODUCTION", | |
| }); | |
| const RCCancelExpireReasonSchema = v.enum({ | |
| UNSUBSCRIBE: "UNSUBSCRIBE", | |
| BILLING_ERROR: "BILLING_ERROR", | |
| DEVELOPER_INITIATED: "DEVELOPER_INITIATED", | |
| PRICE_INCREASE: "PRICE_INCREASE", | |
| CUSTOMER_SUPPORT: "CUSTOMER_SUPPORT", | |
| UNKNOWN: "UNKNOWN", | |
| }); | |
| const RCPurchaseEnvironmentSchema = v.enum({ | |
| in_app_purchase: "in_app_purchase", | |
| admin_api: "admin_api", | |
| }); | |
| const RCVirtualCurrencyAdjustment = v.object({ | |
| amount: v.pipe(v.number(), v.integer()), | |
| currency: v.object({ | |
| code: v.string(), | |
| name: v.string(), | |
| description: v.string(), | |
| }), | |
| }); | |
| const RCSubscriptionLifecycleFieldSchema = v.object({ | |
| ...RCCommonEventFieldSchema.entries, | |
| /** | |
| * Product identifier of the subscription. Please note: For Google Play | |
| * products set up in RevenueCat after February 2023, this identifier has the | |
| * format `<subscription_id>:<base_plan_id>`. | |
| */ | |
| product_id: v.string(), | |
| /** | |
| * Entitlement identifiers of the subscription. It can be `NULL` if the | |
| * `product_id` is not mapped to any entitlements. | |
| */ | |
| entitlement_ids: v.nullable(v.array(v.string())), | |
| /** | |
| * Period type of the transaction. | |
| */ | |
| period_type: RCSubscriptionPeriodTypeSchema, | |
| /** | |
| * Time when the transaction was purchased. Measured in milliseconds since | |
| * Unix epoch. | |
| */ | |
| purchased_at_ms: RCTimestampSchema, | |
| /** | |
| * Expiration of the transaction. Measured in milliseconds since Unix epoch. | |
| * Use this field to determine if a subscription is still active. It can be | |
| * `NULL` for non-subscription purchases. | |
| */ | |
| expiration_at_ms: v.nullable(RCTimestampSchema), | |
| /** | |
| * The time when an Android subscription would resume after being paused. | |
| * Measured in milliseconds since Unix epoch. Only available for Play Store | |
| * subscriptions and `SUBSCRIPTION_PAUSED` events. | |
| */ | |
| auto_resume_at_ms: v.undefinedable(RCTimestampSchema), | |
| /** | |
| * Store the subscription belongs to. | |
| */ | |
| store: RCStoreSchema, | |
| /** | |
| * Store environment. | |
| */ | |
| environment: RCEnvironmentSchema, | |
| /** | |
| * Not available for apps using legacy entitlements. The identifier for the | |
| * offering that was presented to the user during their initial purchase. Can | |
| * be `NULL` if the purchase was made using `purchaseProduct` instead of | |
| * `purchasePackage` or if the purchase was made outside of your app or | |
| * before you integrated RevenueCat. | |
| */ | |
| presented_offering_id: v.nullable(v.string()), | |
| /** | |
| * The USD price of the transaction. Can be `NULL` if the price is unknown, | |
| * and `0` for free trials. Can be negative for refunds. | |
| */ | |
| price: v.nullable(v.number()), | |
| /** | |
| * The ISO 4217 currency code that the product was purchased in. Can be NULL | |
| * if the currency is unknown. | |
| */ | |
| currency: v.nullable(v.string()), | |
| /** | |
| * The price of the transaction in the currency the product was purchased in. | |
| * Can be `NULL` if the price is unknown, and `0` for free trials. Can be | |
| * negative for refunds. | |
| */ | |
| price_in_purchased_currency: v.nullable(v.number()), | |
| /** | |
| * The estimated percentage of the transaction price that was deducted for | |
| * taxes (varies by country and store). Can be `NULL` if the tax percentage is | |
| * unknown. | |
| */ | |
| tax_percentage: v.nullable(v.number()), | |
| /** | |
| * The estimated percentage of the transaction price that was deducted as a | |
| * store commission / processing fee. Can be `NULL` if the commission | |
| * percentage is unknown. | |
| */ | |
| commission_percentage: v.nullable(v.number()), | |
| /** | |
| * Transaction identifier from Apple/Amazon/Google/Stripe. | |
| */ | |
| transaction_id: v.string(), | |
| /** | |
| * `transaction_id` of the original transaction in the subscription from | |
| * Apple/Amazon/Google/Stripe. | |
| */ | |
| original_transaction_id: v.string(), | |
| /** | |
| * Indicates if the user made this purchase or if it was shared to them via | |
| * Family Sharing. Always false for non-Apple purchases. | |
| */ | |
| is_family_share: v.boolean(), | |
| /** | |
| * The ISO 3166 country code that the product was purchased in. The | |
| * two-letter country code (e.g., US, GB, CA) of the app user's location | |
| * (this country code is derived from the last seen request from the SDK for | |
| * the subscriber.) | |
| */ | |
| country_code: v.string(), | |
| /** | |
| * Not available when type is set to `SUBSCRIBER_ALIAS` or `TRANSFER`. The | |
| * code or identifier that the customer used to redeem the transaction. | |
| * Available for App Store and Play Store. | |
| * | |
| * For App Store this property corresponds to the `offerIdentifier`. | |
| * | |
| * For Play Store this corresponds to the `promotionCode`. Can be null if no | |
| * offer code was used for this product. | |
| */ | |
| offer_code: v.string(), | |
| /** | |
| * The number of renewals that this subscription has already gone through. | |
| * Always starts at 1. Trial conversions are counted as renewals. | |
| * `is_trial_conversion` is used to signify whether a transaction was a trial | |
| * conversion. | |
| */ | |
| renewal_number: v.pipe(v.number(), v.integer()), | |
| }); | |
| /* | |
| * Event body. | |
| */ | |
| const RCBillingIssueBodySchema = v.object({ | |
| type: v.literal(RCEventTypeSchema.enum.BILLING_ISSUE), | |
| ...RCSubscriptionLifecycleFieldSchema.entries, | |
| /** | |
| * Only available for `BILLING_ISSUE` events. The time that the grace period | |
| * for the subscription would expire. Measured in milliseconds since Unix | |
| * epoch. Use this field to determine if the user is currently in a grace | |
| * period. It can be `NULL` if the subscription does not have a grace period. | |
| */ | |
| grace_period_expiration_at_ms: RCTimestampSchema, | |
| }); | |
| const RCRenewalBodySchema = v.object({ | |
| type: v.literal(RCEventTypeSchema.enum.RENEWAL), | |
| ...RCSubscriptionLifecycleFieldSchema.entries, | |
| /** | |
| * Only available for `RENEWAL` events. Whether the previous transaction was a | |
| * free trial or not. | |
| */ | |
| is_trial_conversion: v.boolean(), | |
| }); | |
| const RCCancellationBodySchema = v.object({ | |
| type: v.literal(RCEventTypeSchema.enum.CANCELLATION), | |
| ...RCSubscriptionLifecycleFieldSchema.entries, | |
| /** | |
| * Only available for `CANCELLATION` events. | |
| */ | |
| cancel_reason: RCCancelExpireReasonSchema, | |
| }); | |
| const RCExpirationBodySchema = v.object({ | |
| type: v.literal(RCEventTypeSchema.enum.EXPIRATION), | |
| ...RCSubscriptionLifecycleFieldSchema.entries, | |
| /** | |
| * Only available for `EXPIRATION` events. | |
| */ | |
| expiration_reason: RCCancelExpireReasonSchema, | |
| }); | |
| const RCProductChangeBodySchema = v.object({ | |
| type: v.literal(RCEventTypeSchema.enum.PRODUCT_CHANGE), | |
| ...RCSubscriptionLifecycleFieldSchema.entries, | |
| /** | |
| * Product identifier of the new product the subscriber has switched to. Only | |
| * available in `PRODUCT_CHANGE` events for Play Store subscriptions with the | |
| * DEFERRED replacement mode and App Store subscriptions | |
| */ | |
| new_product_id: v.undefinedable(v.string()), | |
| }); | |
| const RCTransferBodySchema = v.object({ | |
| type: v.literal(RCEventTypeSchema.enum.TRANSFER), | |
| ...v.omit(RCSubscriptionLifecycleFieldSchema, [ | |
| "offer_code", | |
| "app_user_id", | |
| "original_app_user_id", | |
| ]).entries, | |
| /** | |
| * This field is only available when type is set to `TRANSFER`. App User ID(s) | |
| * that transactions and entitlements are being taken from, and granted to | |
| * `transferred_to`. | |
| */ | |
| transferred_from: v.array(v.string()), | |
| /** | |
| * This field is only available when type is set to `TRANSFER`. App User ID(s) | |
| * that are receiving the transactions and entitlements taken from | |
| * `transferred_from`. | |
| */ | |
| transferred_to: v.array(v.string()), | |
| }); | |
| const RCSubscriberAliasBodySchema = v.object({ | |
| type: v.literal(RCEventTypeSchema.enum.SUBSCRIBER_ALIAS), | |
| ...v.omit(RCSubscriptionLifecycleFieldSchema, ["offer_code"]).entries, | |
| }); | |
| const RCVirtualCurrencyTransactionBodySchema = v.object({ | |
| type: v.literal(RCEventTypeSchema.enum.VIRTUAL_CURRENCY_TRANSACTION), | |
| ...RCCommonEventFieldSchema.entries, | |
| adjustments: v.array(RCVirtualCurrencyAdjustment), | |
| product_display_name: v.string(), | |
| purchase_environment: RCEnvironmentSchema, | |
| source: RCPurchaseEnvironmentSchema, | |
| virtual_currency_transaction_id: v.string(), | |
| }); | |
| const RCNormalSubscriptionEventTypeSchema = v.union([ | |
| v.literal(RCEventTypeSchema.enum.EXPERIMENT_ENROLLMENT), | |
| v.literal(RCEventTypeSchema.enum.INITIAL_PURCHASE), | |
| v.literal(RCEventTypeSchema.enum.INVOICE_ISSUANCE), | |
| v.literal(RCEventTypeSchema.enum.NON_RENEWING_PURCHASE), | |
| v.literal(RCEventTypeSchema.enum.REFUND_REVERSED), | |
| v.literal(RCEventTypeSchema.enum.SUBSCRIPTION_EXTENDED), | |
| v.literal(RCEventTypeSchema.enum.SUBSCRIPTION_PAUSED), | |
| v.literal(RCEventTypeSchema.enum.TEMPORARY_ENTITLEMENT_GRANT), | |
| v.literal(RCEventTypeSchema.enum.TEST), | |
| v.literal(RCEventTypeSchema.enum.UNCANCELLATION), | |
| ]); | |
| export type RCNormalSubscriptionEventType = v.InferInput< | |
| typeof RCNormalSubscriptionEventTypeSchema | |
| >; | |
| const RCNormalSubscriptionLifecycleBodySchema = v.object({ | |
| type: RCNormalSubscriptionEventTypeSchema, | |
| ...RCSubscriptionLifecycleFieldSchema.entries, | |
| }); | |
| export type RCNormalSubscriptionLifecycleBody = v.InferInput< | |
| typeof RCNormalSubscriptionLifecycleBodySchema | |
| >; | |
| export const RCEventBodySchema = v.variant("type", [ | |
| RCNormalSubscriptionLifecycleBodySchema, | |
| RCBillingIssueBodySchema, | |
| RCRenewalBodySchema, | |
| RCCancellationBodySchema, | |
| RCExpirationBodySchema, | |
| RCProductChangeBodySchema, | |
| RCTransferBodySchema, | |
| RCSubscriberAliasBodySchema, | |
| RCVirtualCurrencyTransactionBodySchema, | |
| ]); | |
| export type RCEventBody = v.InferInput<typeof RCEventBodySchema>; | |
| export const RCEventSchema = v.object({ | |
| api_version: v.string(), | |
| event: RCEventBodySchema, | |
| }); | |
| export type RCEvent = v.InferInput<typeof RCEventSchema>; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment