diff --git a/apps/docs/content/docs/en/tools/revenuecat.mdx b/apps/docs/content/docs/en/tools/revenuecat.mdx index ad4b42ee4a8..ebd9814dd88 100644 --- a/apps/docs/content/docs/en/tools/revenuecat.mdx +++ b/apps/docs/content/docs/en/tools/revenuecat.mdx @@ -51,8 +51,10 @@ Retrieve subscriber information by app user ID | --------- | ---- | ----------- | | `subscriber` | object | The subscriber object with subscriptions and entitlements | | ↳ `first_seen` | string | ISO 8601 date when subscriber was first seen | +| ↳ `last_seen` | string | ISO 8601 date when subscriber was last seen | | ↳ `original_app_user_id` | string | Original app user ID | -| ↳ `original_purchase_date` | string | ISO 8601 date of original purchase | +| ↳ `original_application_version` | string | iOS only. First App Store version of your app the customer installed | +| ↳ `original_purchase_date` | string | iOS only. Date the app was first purchased/downloaded | | ↳ `management_url` | string | URL for managing the subscriber subscriptions | | ↳ `subscriptions` | object | Map of product identifiers to subscription objects | | ↳ `store_transaction_id` | string | Store transaction identifier | @@ -71,16 +73,13 @@ Retrieve subscriber information by app user ID | ↳ `auto_resume_date` | string | ISO 8601 date when a paused subscription will auto-resume | | ↳ `product_plan_identifier` | string | Google Play base plan identifier \(for products set up after Feb 2023\) | | ↳ `entitlements` | object | Map of entitlement identifiers to entitlement objects | -| ↳ `grant_date` | string | ISO 8601 grant date | -| ↳ `expires_date` | string | ISO 8601 expiration date | +| ↳ `expires_date` | string | ISO 8601 expiration date \(null for non-expiring entitlements\) | +| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date | | ↳ `product_identifier` | string | Product identifier | -| ↳ `is_active` | boolean | Whether the entitlement is active | -| ↳ `will_renew` | boolean | Whether the entitlement will renew | -| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional\) | | ↳ `purchase_date` | string | ISO 8601 date of the latest purchase or renewal | -| ↳ `store` | string | Store the entitlement was granted from | -| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date | | ↳ `non_subscriptions` | object | Map of non-subscription product identifiers to arrays of purchase objects | +| ↳ `other_purchases` | object | Other purchases attached to the subscriber | +| ↳ `subscriber_attributes` | object | Custom attributes set on the subscriber. Only returned when using a secret API key | | `metadata` | object | Subscriber summary metadata | | ↳ `app_user_id` | string | The app user ID | | ↳ `first_seen` | string | ISO 8601 date when the subscriber was first seen | @@ -115,21 +114,29 @@ Record a purchase (receipt) for a subscriber via the REST API | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | RevenueCat API key \(public or secret\) | | `appUserId` | string | Yes | The app user ID of the subscriber | -| `fetchToken` | string | Yes | The receipt token or purchase token from the store \(App Store receipt, Google Play purchase token, or Stripe subscription ID\) | -| `productId` | string | Yes | The product identifier for the purchase | -| `price` | number | No | The price of the product in the currency specified | -| `currency` | string | No | ISO 4217 currency code \(e.g., USD, EUR\) | -| `isRestore` | boolean | No | Whether this is a restore of a previous purchase | -| `platform` | string | No | Platform of the purchase \(ios, android, amazon, macos, stripe\). Required for Stripe and Paddle purchases. | +| `fetchToken` | string | Yes | For iOS, the base64-encoded receipt \(or JWSTransaction for StoreKit2\); for Android the purchase token; for Amazon the receipt; for Stripe the subscription ID or Checkout Session ID; for Roku the transaction ID; for Paddle the subscription ID or transaction ID | +| `productId` | string | No | Apple, Google, Amazon, Roku, or Paddle product identifier or SKU. Required for Google. | +| `price` | number | No | Price of the product. Required if you provide a currency. | +| `currency` | string | No | ISO 4217 currency code \(e.g., USD, EUR\). Required if you provide a price. | +| `isRestore` | boolean | No | Deprecated. Triggers configured restore behavior for shared fetch tokens. | +| `presentedOfferingIdentifier` | string | No | Identifier of the offering presented to the customer at the time of purchase. Attached to new transactions in this fetch token and exposed in ETL exports and webhooks. | +| `paymentMode` | string | No | Payment mode for the introductory period. One of: pay_as_you_go, pay_up_front, free_trial. Defaults to free_trial when an introductory period is detected and no value is provided. | +| `introductoryPrice` | number | No | Introductory price paid \(if any\). | +| `attributes` | json | No | JSON object of subscriber attributes to set alongside the purchase. Each key maps to \{"value": string, "updated_at_ms": number\}. | +| `updatedAtMs` | number | No | UNIX epoch in milliseconds used to resolve attribute conflicts at the request level. | +| `platform` | string | Yes | Platform of the purchase. One of: ios, android, amazon, macos, uikitformac, stripe, roku, paddle. Sent as the X-Platform header \(required by RevenueCat\). | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | +| `customer` | object | Customer object returned at the top level of POST /v1/receipts \(first_seen, last_seen, original_app_user_id, original_application_version, original_sdk_version, management_url, entitlements, original_purchase_date, request_date\). Null when the response uses the `value`-wrapped envelope. | | `subscriber` | object | The updated subscriber object after recording the purchase | | ↳ `first_seen` | string | ISO 8601 date when subscriber was first seen | +| ↳ `last_seen` | string | ISO 8601 date when subscriber was last seen | | ↳ `original_app_user_id` | string | Original app user ID | -| ↳ `original_purchase_date` | string | ISO 8601 date of original purchase | +| ↳ `original_application_version` | string | iOS only. First App Store version of your app the customer installed | +| ↳ `original_purchase_date` | string | iOS only. Date the app was first purchased/downloaded | | ↳ `management_url` | string | URL for managing the subscriber subscriptions | | ↳ `subscriptions` | object | Map of product identifiers to subscription objects | | ↳ `store_transaction_id` | string | Store transaction identifier | @@ -148,16 +155,13 @@ Record a purchase (receipt) for a subscriber via the REST API | ↳ `auto_resume_date` | string | ISO 8601 date when a paused subscription will auto-resume | | ↳ `product_plan_identifier` | string | Google Play base plan identifier \(for products set up after Feb 2023\) | | ↳ `entitlements` | object | Map of entitlement identifiers to entitlement objects | -| ↳ `grant_date` | string | ISO 8601 grant date | -| ↳ `expires_date` | string | ISO 8601 expiration date | +| ↳ `expires_date` | string | ISO 8601 expiration date \(null for non-expiring entitlements\) | +| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date | | ↳ `product_identifier` | string | Product identifier | -| ↳ `is_active` | boolean | Whether the entitlement is active | -| ↳ `will_renew` | boolean | Whether the entitlement will renew | -| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional\) | | ↳ `purchase_date` | string | ISO 8601 date of the latest purchase or renewal | -| ↳ `store` | string | Store the entitlement was granted from | -| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date | | ↳ `non_subscriptions` | object | Map of non-subscription product identifiers to arrays of purchase objects | +| ↳ `other_purchases` | object | Other purchases attached to the subscriber | +| ↳ `subscriber_attributes` | object | Custom attributes set on the subscriber. Only returned when using a secret API key | ### `revenuecat_grant_entitlement` @@ -170,7 +174,8 @@ Grant a promotional entitlement to a subscriber | `apiKey` | string | Yes | RevenueCat secret API key \(sk_...\) | | `appUserId` | string | Yes | The app user ID of the subscriber | | `entitlementIdentifier` | string | Yes | The entitlement identifier to grant | -| `duration` | string | Yes | Duration of the entitlement \(daily, three_day, weekly, monthly, two_month, three_month, six_month, yearly, lifetime\) | +| `duration` | string | No | Duration of the entitlement. Provide either duration or endTimeMs. One of: daily, three_day, weekly, two_week, monthly, two_month, three_month, six_month, yearly, lifetime | +| `endTimeMs` | number | No | Absolute end time in milliseconds since Unix epoch. Use instead of duration to grant the entitlement until a specific timestamp. | | `startTimeMs` | number | No | Optional start time in milliseconds since Unix epoch. Set to a past time to achieve custom durations shorter than daily. | #### Output @@ -179,8 +184,10 @@ Grant a promotional entitlement to a subscriber | --------- | ---- | ----------- | | `subscriber` | object | The updated subscriber object after granting the entitlement | | ↳ `first_seen` | string | ISO 8601 date when subscriber was first seen | +| ↳ `last_seen` | string | ISO 8601 date when subscriber was last seen | | ↳ `original_app_user_id` | string | Original app user ID | -| ↳ `original_purchase_date` | string | ISO 8601 date of original purchase | +| ↳ `original_application_version` | string | iOS only. First App Store version of your app the customer installed | +| ↳ `original_purchase_date` | string | iOS only. Date the app was first purchased/downloaded | | ↳ `management_url` | string | URL for managing the subscriber subscriptions | | ↳ `subscriptions` | object | Map of product identifiers to subscription objects | | ↳ `store_transaction_id` | string | Store transaction identifier | @@ -199,16 +206,13 @@ Grant a promotional entitlement to a subscriber | ↳ `auto_resume_date` | string | ISO 8601 date when a paused subscription will auto-resume | | ↳ `product_plan_identifier` | string | Google Play base plan identifier \(for products set up after Feb 2023\) | | ↳ `entitlements` | object | Map of entitlement identifiers to entitlement objects | -| ↳ `grant_date` | string | ISO 8601 grant date | -| ↳ `expires_date` | string | ISO 8601 expiration date | +| ↳ `expires_date` | string | ISO 8601 expiration date \(null for non-expiring entitlements\) | +| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date | | ↳ `product_identifier` | string | Product identifier | -| ↳ `is_active` | boolean | Whether the entitlement is active | -| ↳ `will_renew` | boolean | Whether the entitlement will renew | -| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional\) | | ↳ `purchase_date` | string | ISO 8601 date of the latest purchase or renewal | -| ↳ `store` | string | Store the entitlement was granted from | -| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date | | ↳ `non_subscriptions` | object | Map of non-subscription product identifiers to arrays of purchase objects | +| ↳ `other_purchases` | object | Other purchases attached to the subscriber | +| ↳ `subscriber_attributes` | object | Custom attributes set on the subscriber. Only returned when using a secret API key | ### `revenuecat_revoke_entitlement` @@ -228,8 +232,10 @@ Revoke all promotional entitlements for a specific entitlement identifier | --------- | ---- | ----------- | | `subscriber` | object | The updated subscriber object after revoking the entitlement | | ↳ `first_seen` | string | ISO 8601 date when subscriber was first seen | +| ↳ `last_seen` | string | ISO 8601 date when subscriber was last seen | | ↳ `original_app_user_id` | string | Original app user ID | -| ↳ `original_purchase_date` | string | ISO 8601 date of original purchase | +| ↳ `original_application_version` | string | iOS only. First App Store version of your app the customer installed | +| ↳ `original_purchase_date` | string | iOS only. Date the app was first purchased/downloaded | | ↳ `management_url` | string | URL for managing the subscriber subscriptions | | ↳ `subscriptions` | object | Map of product identifiers to subscription objects | | ↳ `store_transaction_id` | string | Store transaction identifier | @@ -248,16 +254,13 @@ Revoke all promotional entitlements for a specific entitlement identifier | ↳ `auto_resume_date` | string | ISO 8601 date when a paused subscription will auto-resume | | ↳ `product_plan_identifier` | string | Google Play base plan identifier \(for products set up after Feb 2023\) | | ↳ `entitlements` | object | Map of entitlement identifiers to entitlement objects | -| ↳ `grant_date` | string | ISO 8601 grant date | -| ↳ `expires_date` | string | ISO 8601 expiration date | +| ↳ `expires_date` | string | ISO 8601 expiration date \(null for non-expiring entitlements\) | +| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date | | ↳ `product_identifier` | string | Product identifier | -| ↳ `is_active` | boolean | Whether the entitlement is active | -| ↳ `will_renew` | boolean | Whether the entitlement will renew | -| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional\) | | ↳ `purchase_date` | string | ISO 8601 date of the latest purchase or renewal | -| ↳ `store` | string | Store the entitlement was granted from | -| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date | | ↳ `non_subscriptions` | object | Map of non-subscription product identifiers to arrays of purchase objects | +| ↳ `other_purchases` | object | Other purchases attached to the subscriber | +| ↳ `subscriber_attributes` | object | Custom attributes set on the subscriber. Only returned when using a secret API key | ### `revenuecat_list_offerings` @@ -296,7 +299,7 @@ Update custom subscriber attributes (e.g., $email, $displayName, or custom key-v | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | RevenueCat secret API key \(sk_...\) | | `appUserId` | string | Yes | The app user ID of the subscriber | -| `attributes` | json | Yes | JSON object of attributes to set. Each key maps to an object with a "value" field. Example: \{"$email": \{"value": "user@example.com"\}, "$displayName": \{"value": "John"\}\} | +| `attributes` | json | Yes | JSON object of attributes to set. Each key maps to an object with "value" \(string; null or empty deletes the attribute\) and "updated_at_ms" \(Unix epoch ms used for conflict resolution — required\). Example: \{"$email": \{"value": "user@example.com", "updated_at_ms": 1709195668093\}\} | #### Output @@ -304,6 +307,37 @@ Update custom subscriber attributes (e.g., $email, $displayName, or custom key-v | --------- | ---- | ----------- | | `updated` | boolean | Whether the subscriber attributes were successfully updated | | `app_user_id` | string | The app user ID of the updated subscriber | +| `subscriber` | object | The updated subscriber object after applying the attribute changes | +| ↳ `first_seen` | string | ISO 8601 date when subscriber was first seen | +| ↳ `last_seen` | string | ISO 8601 date when subscriber was last seen | +| ↳ `original_app_user_id` | string | Original app user ID | +| ↳ `original_application_version` | string | iOS only. First App Store version of your app the customer installed | +| ↳ `original_purchase_date` | string | iOS only. Date the app was first purchased/downloaded | +| ↳ `management_url` | string | URL for managing the subscriber subscriptions | +| ↳ `subscriptions` | object | Map of product identifiers to subscription objects | +| ↳ `store_transaction_id` | string | Store transaction identifier | +| ↳ `original_transaction_id` | string | Original transaction identifier | +| ↳ `purchase_date` | string | ISO 8601 purchase date | +| ↳ `original_purchase_date` | string | ISO 8601 date of the original purchase | +| ↳ `expires_date` | string | ISO 8601 expiration date | +| ↳ `is_sandbox` | boolean | Whether this is a sandbox purchase | +| ↳ `unsubscribe_detected_at` | string | ISO 8601 date when unsubscribe was detected | +| ↳ `billing_issues_detected_at` | string | ISO 8601 date when billing issues were detected | +| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date | +| ↳ `ownership_type` | string | Ownership type \(purchased, family_shared\) | +| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional, prepaid\) | +| ↳ `store` | string | Store the subscription was purchased from \(app_store, play_store, stripe, etc.\) | +| ↳ `refunded_at` | string | ISO 8601 date when subscription was refunded | +| ↳ `auto_resume_date` | string | ISO 8601 date when a paused subscription will auto-resume | +| ↳ `product_plan_identifier` | string | Google Play base plan identifier \(for products set up after Feb 2023\) | +| ↳ `entitlements` | object | Map of entitlement identifiers to entitlement objects | +| ↳ `expires_date` | string | ISO 8601 expiration date \(null for non-expiring entitlements\) | +| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date | +| ↳ `product_identifier` | string | Product identifier | +| ↳ `purchase_date` | string | ISO 8601 date of the latest purchase or renewal | +| ↳ `non_subscriptions` | object | Map of non-subscription product identifiers to arrays of purchase objects | +| ↳ `other_purchases` | object | Other purchases attached to the subscriber | +| ↳ `subscriber_attributes` | object | Custom attributes set on the subscriber. Only returned when using a secret API key | ### `revenuecat_defer_google_subscription` @@ -316,7 +350,8 @@ Defer a Google Play subscription by extending its billing date by a number of da | `apiKey` | string | Yes | RevenueCat secret API key \(sk_...\) | | `appUserId` | string | Yes | The app user ID of the subscriber | | `productId` | string | Yes | The Google Play product identifier of the subscription to defer \(use the part before the colon for products set up after Feb 2023\) | -| `extendByDays` | number | Yes | Number of days to extend the subscription by \(1-365\) | +| `extendByDays` | number | No | Number of days to extend the subscription by \(1-365\). Provide either extendByDays or expiryTimeMs. | +| `expiryTimeMs` | number | No | Absolute new expiry time in milliseconds since Unix epoch. Use instead of extendByDays to set an exact expiry. | #### Output @@ -324,8 +359,10 @@ Defer a Google Play subscription by extending its billing date by a number of da | --------- | ---- | ----------- | | `subscriber` | object | The updated subscriber object after deferring the Google subscription | | ↳ `first_seen` | string | ISO 8601 date when subscriber was first seen | +| ↳ `last_seen` | string | ISO 8601 date when subscriber was last seen | | ↳ `original_app_user_id` | string | Original app user ID | -| ↳ `original_purchase_date` | string | ISO 8601 date of original purchase | +| ↳ `original_application_version` | string | iOS only. First App Store version of your app the customer installed | +| ↳ `original_purchase_date` | string | iOS only. Date the app was first purchased/downloaded | | ↳ `management_url` | string | URL for managing the subscriber subscriptions | | ↳ `subscriptions` | object | Map of product identifiers to subscription objects | | ↳ `store_transaction_id` | string | Store transaction identifier | @@ -344,20 +381,17 @@ Defer a Google Play subscription by extending its billing date by a number of da | ↳ `auto_resume_date` | string | ISO 8601 date when a paused subscription will auto-resume | | ↳ `product_plan_identifier` | string | Google Play base plan identifier \(for products set up after Feb 2023\) | | ↳ `entitlements` | object | Map of entitlement identifiers to entitlement objects | -| ↳ `grant_date` | string | ISO 8601 grant date | -| ↳ `expires_date` | string | ISO 8601 expiration date | +| ↳ `expires_date` | string | ISO 8601 expiration date \(null for non-expiring entitlements\) | +| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date | | ↳ `product_identifier` | string | Product identifier | -| ↳ `is_active` | boolean | Whether the entitlement is active | -| ↳ `will_renew` | boolean | Whether the entitlement will renew | -| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional\) | | ↳ `purchase_date` | string | ISO 8601 date of the latest purchase or renewal | -| ↳ `store` | string | Store the entitlement was granted from | -| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date | | ↳ `non_subscriptions` | object | Map of non-subscription product identifiers to arrays of purchase objects | +| ↳ `other_purchases` | object | Other purchases attached to the subscriber | +| ↳ `subscriber_attributes` | object | Custom attributes set on the subscriber. Only returned when using a secret API key | ### `revenuecat_refund_google_subscription` -Refund and optionally revoke a Google Play subscription (Google Play only) +Refund a specific store transaction by its store transaction identifier and revoke access (subscription or non-subscription, last 365 days) #### Input @@ -365,7 +399,7 @@ Refund and optionally revoke a Google Play subscription (Google Play only) | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | RevenueCat secret API key \(sk_...\) | | `appUserId` | string | Yes | The app user ID of the subscriber | -| `productId` | string | Yes | The Google Play product identifier of the subscription to refund | +| `storeTransactionId` | string | Yes | The store transaction identifier of the purchase to refund \(e.g., GPA.3309-9122-6177-45730 for Google Play\) | #### Output @@ -373,8 +407,10 @@ Refund and optionally revoke a Google Play subscription (Google Play only) | --------- | ---- | ----------- | | `subscriber` | object | The updated subscriber object after refunding the Google subscription | | ↳ `first_seen` | string | ISO 8601 date when subscriber was first seen | +| ↳ `last_seen` | string | ISO 8601 date when subscriber was last seen | | ↳ `original_app_user_id` | string | Original app user ID | -| ↳ `original_purchase_date` | string | ISO 8601 date of original purchase | +| ↳ `original_application_version` | string | iOS only. First App Store version of your app the customer installed | +| ↳ `original_purchase_date` | string | iOS only. Date the app was first purchased/downloaded | | ↳ `management_url` | string | URL for managing the subscriber subscriptions | | ↳ `subscriptions` | object | Map of product identifiers to subscription objects | | ↳ `store_transaction_id` | string | Store transaction identifier | @@ -393,16 +429,13 @@ Refund and optionally revoke a Google Play subscription (Google Play only) | ↳ `auto_resume_date` | string | ISO 8601 date when a paused subscription will auto-resume | | ↳ `product_plan_identifier` | string | Google Play base plan identifier \(for products set up after Feb 2023\) | | ↳ `entitlements` | object | Map of entitlement identifiers to entitlement objects | -| ↳ `grant_date` | string | ISO 8601 grant date | -| ↳ `expires_date` | string | ISO 8601 expiration date | +| ↳ `expires_date` | string | ISO 8601 expiration date \(null for non-expiring entitlements\) | +| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date | | ↳ `product_identifier` | string | Product identifier | -| ↳ `is_active` | boolean | Whether the entitlement is active | -| ↳ `will_renew` | boolean | Whether the entitlement will renew | -| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional\) | | ↳ `purchase_date` | string | ISO 8601 date of the latest purchase or renewal | -| ↳ `store` | string | Store the entitlement was granted from | -| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date | | ↳ `non_subscriptions` | object | Map of non-subscription product identifiers to arrays of purchase objects | +| ↳ `other_purchases` | object | Other purchases attached to the subscriber | +| ↳ `subscriber_attributes` | object | Custom attributes set on the subscriber. Only returned when using a secret API key | ### `revenuecat_revoke_google_subscription` @@ -422,8 +455,10 @@ Immediately revoke access to a Google Play subscription and issue a refund (Goog | --------- | ---- | ----------- | | `subscriber` | object | The updated subscriber object after revoking the Google subscription | | ↳ `first_seen` | string | ISO 8601 date when subscriber was first seen | +| ↳ `last_seen` | string | ISO 8601 date when subscriber was last seen | | ↳ `original_app_user_id` | string | Original app user ID | -| ↳ `original_purchase_date` | string | ISO 8601 date of original purchase | +| ↳ `original_application_version` | string | iOS only. First App Store version of your app the customer installed | +| ↳ `original_purchase_date` | string | iOS only. Date the app was first purchased/downloaded | | ↳ `management_url` | string | URL for managing the subscriber subscriptions | | ↳ `subscriptions` | object | Map of product identifiers to subscription objects | | ↳ `store_transaction_id` | string | Store transaction identifier | @@ -442,15 +477,12 @@ Immediately revoke access to a Google Play subscription and issue a refund (Goog | ↳ `auto_resume_date` | string | ISO 8601 date when a paused subscription will auto-resume | | ↳ `product_plan_identifier` | string | Google Play base plan identifier \(for products set up after Feb 2023\) | | ↳ `entitlements` | object | Map of entitlement identifiers to entitlement objects | -| ↳ `grant_date` | string | ISO 8601 grant date | -| ↳ `expires_date` | string | ISO 8601 expiration date | +| ↳ `expires_date` | string | ISO 8601 expiration date \(null for non-expiring entitlements\) | +| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date | | ↳ `product_identifier` | string | Product identifier | -| ↳ `is_active` | boolean | Whether the entitlement is active | -| ↳ `will_renew` | boolean | Whether the entitlement will renew | -| ↳ `period_type` | string | Period type \(normal, trial, intro, promotional\) | | ↳ `purchase_date` | string | ISO 8601 date of the latest purchase or renewal | -| ↳ `store` | string | Store the entitlement was granted from | -| ↳ `grace_period_expires_date` | string | ISO 8601 grace period expiration date | | ↳ `non_subscriptions` | object | Map of non-subscription product identifiers to arrays of purchase objects | +| ↳ `other_purchases` | object | Other purchases attached to the subscriber | +| ↳ `subscriber_attributes` | object | Custom attributes set on the subscriber. Only returned when using a secret API key | diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 9a1513dc6f8..08fa8bb80d7 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -10748,7 +10748,7 @@ }, { "name": "Refund Google Subscription", - "description": "Refund and optionally revoke a Google Play subscription (Google Play only)" + "description": "Refund a specific store transaction by its store transaction identifier and revoke access (subscription or non-subscription, last 365 days)" }, { "name": "Revoke Google Subscription", @@ -14173,7 +14173,7 @@ "description": "Hire a pre-hire into an employee position. Converts an applicant into an active employee record with position, start date, and manager assignment." }, { - "name": "Update Worker", + "name": "Update Personal Information", "description": "Update fields on an existing worker record in Workday." }, { diff --git a/apps/sim/blocks/blocks/revenuecat.ts b/apps/sim/blocks/blocks/revenuecat.ts index c99b5cf6917..316a4fe8425 100644 --- a/apps/sim/blocks/blocks/revenuecat.ts +++ b/apps/sim/blocks/blocks/revenuecat.ts @@ -72,6 +72,7 @@ export const RevenueCatBlock: BlockConfig = { { label: 'Daily', id: 'daily' }, { label: '3 Days', id: 'three_day' }, { label: 'Weekly', id: 'weekly' }, + { label: '2 Weeks', id: 'two_week' }, { label: 'Monthly', id: 'monthly' }, { label: '2 Months', id: 'two_month' }, { label: '3 Months', id: 'three_month' }, @@ -85,6 +86,28 @@ export const RevenueCatBlock: BlockConfig = { value: 'grant_entitlement', }, }, + { + id: 'endTimeMs', + title: 'End Time (ms)', + type: 'short-input', + placeholder: 'Optional absolute end time in ms since epoch', + condition: { + field: 'operation', + value: 'grant_entitlement', + }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate a Unix epoch timestamp in milliseconds based on the user's description. +The timestamp should represent the absolute end time for the entitlement. +Examples: +- "in 7 days" -> current time plus 604800000 milliseconds +- "next month" -> current time plus 2592000000 milliseconds +- "end of 2026" -> 1798761600000 + +Return ONLY the numeric timestamp, no text.`, + }, + }, { id: 'startTimeMs', title: 'Start Time (ms)', @@ -124,9 +147,10 @@ Return ONLY the numeric timestamp, no text.`, }, { id: 'productId', - title: 'Product ID', + title: 'Product ID / Store Transaction ID', type: 'short-input', - placeholder: 'Product identifier', + placeholder: + 'Product ID, or store transaction ID for refunds (e.g., GPA.3309-9122-6177-45730)', condition: { field: 'operation', value: [ @@ -139,7 +163,6 @@ Return ONLY the numeric timestamp, no text.`, required: { field: 'operation', value: [ - 'create_purchase', 'defer_google_subscription', 'refund_google_subscription', 'revoke_google_subscription', @@ -168,6 +191,61 @@ Return ONLY the numeric timestamp, no text.`, }, mode: 'advanced', }, + { + id: 'presentedOfferingIdentifier', + title: 'Presented Offering ID', + type: 'short-input', + placeholder: 'Offering identifier shown to the user', + condition: { + field: 'operation', + value: 'create_purchase', + }, + mode: 'advanced', + }, + { + id: 'paymentMode', + title: 'Payment Mode', + type: 'dropdown', + options: [ + { label: 'Pay As You Go', id: 'pay_as_you_go' }, + { label: 'Pay Up Front', id: 'pay_up_front' }, + { label: 'Free Trial', id: 'free_trial' }, + ], + condition: { + field: 'operation', + value: 'create_purchase', + }, + mode: 'advanced', + }, + { + id: 'introductoryPrice', + title: 'Introductory Price', + type: 'short-input', + placeholder: 'e.g., 0.99', + condition: { + field: 'operation', + value: 'create_purchase', + }, + mode: 'advanced', + }, + { + id: 'updatedAtMs', + title: 'Updated At (ms)', + type: 'short-input', + placeholder: 'Unix epoch ms used to resolve attribute conflicts', + condition: { + field: 'operation', + value: 'create_purchase', + }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate a Unix epoch timestamp in milliseconds based on the user's description. +Used by RevenueCat to resolve attribute conflicts on a posted purchase. + +Return ONLY the numeric timestamp, no text.`, + }, + }, { id: 'isRestore', title: 'Is Restore', @@ -192,13 +270,19 @@ Return ONLY the numeric timestamp, no text.`, { label: 'Android', id: 'android' }, { label: 'Amazon', id: 'amazon' }, { label: 'macOS', id: 'macos' }, + { label: 'UIKit for Mac', id: 'uikitformac' }, { label: 'Stripe', id: 'stripe' }, + { label: 'Roku', id: 'roku' }, + { label: 'Paddle', id: 'paddle' }, ], condition: { field: 'operation', value: 'create_purchase', }, - mode: 'advanced', + required: { + field: 'operation', + value: 'create_purchase', + }, }, { id: 'attributes', @@ -207,7 +291,7 @@ Return ONLY the numeric timestamp, no text.`, placeholder: '{"$email": {"value": "user@example.com"}}', condition: { field: 'operation', - value: 'update_subscriber_attributes', + value: ['update_subscriber_attributes', 'create_purchase'], }, required: { field: 'operation', @@ -238,10 +322,24 @@ Return ONLY valid JSON.`, field: 'operation', value: 'defer_google_subscription', }, - required: { + }, + { + id: 'expiryTimeMs', + title: 'Expiry Time (ms)', + type: 'short-input', + placeholder: 'Absolute new expiry time in ms since epoch', + condition: { field: 'operation', value: 'defer_google_subscription', }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate a Unix epoch timestamp in milliseconds based on the user's description. +The timestamp should represent the new absolute expiry time of the subscription. + +Return ONLY the numeric timestamp, no text.`, + }, }, { id: 'platform', @@ -251,13 +349,15 @@ Return ONLY valid JSON.`, { label: 'iOS', id: 'ios' }, { label: 'Android', id: 'android' }, { label: 'Amazon', id: 'amazon' }, - { label: 'macOS', id: 'macos' }, { label: 'Stripe', id: 'stripe' }, + { label: 'Roku', id: 'roku' }, + { label: 'Paddle', id: 'paddle' }, ], condition: { field: 'operation', value: 'list_offerings', }, + mode: 'advanced', }, ], tools: { @@ -274,23 +374,42 @@ Return ONLY valid JSON.`, 'revenuecat_revoke_google_subscription', ], config: { - tool: (params) => { + tool: (params) => `revenuecat_${params.operation}`, + params: (params) => { + const next: Record = { ...params } if (params.purchasePlatform && params.operation === 'create_purchase') { - params.platform = params.purchasePlatform + next.platform = params.purchasePlatform + } + next.purchasePlatform = undefined + if (params.productId && params.operation === 'refund_google_subscription') { + next.storeTransactionId = params.productId + next.productId = undefined } - if (params.isRestore !== undefined) { - params.isRestore = params.isRestore === 'true' + if (params.isRestore !== undefined && params.isRestore !== '') { + next.isRestore = params.isRestore === true || params.isRestore === 'true' } if (params.price !== undefined && params.price !== '') { - params.price = Number(params.price) + next.price = Number(params.price) } if (params.extendByDays !== undefined && params.extendByDays !== '') { - params.extendByDays = Number(params.extendByDays) + next.extendByDays = Number(params.extendByDays) } if (params.startTimeMs !== undefined && params.startTimeMs !== '') { - params.startTimeMs = Number(params.startTimeMs) + next.startTimeMs = Number(params.startTimeMs) + } + if (params.endTimeMs !== undefined && params.endTimeMs !== '') { + next.endTimeMs = Number(params.endTimeMs) + } + if (params.expiryTimeMs !== undefined && params.expiryTimeMs !== '') { + next.expiryTimeMs = Number(params.expiryTimeMs) + } + if (params.introductoryPrice !== undefined && params.introductoryPrice !== '') { + next.introductoryPrice = Number(params.introductoryPrice) } - return `revenuecat_${params.operation}` + if (params.updatedAtMs !== undefined && params.updatedAtMs !== '') { + next.updatedAtMs = Number(params.updatedAtMs) + } + return next }, }, }, @@ -302,28 +421,61 @@ Return ONLY valid JSON.`, duration: { type: 'string', description: 'Promotional entitlement duration' }, startTimeMs: { type: 'number', description: 'Custom start time in ms since epoch' }, fetchToken: { type: 'string', description: 'Store receipt or purchase token' }, - productId: { type: 'string', description: 'Product identifier' }, + productId: { + type: 'string', + description: 'Product identifier (or store transaction ID for refunds)', + }, price: { type: 'number', description: 'Product price' }, currency: { type: 'string', description: 'ISO 4217 currency code' }, isRestore: { type: 'boolean', description: 'Whether this is a restore purchase' }, - purchasePlatform: { type: 'string', description: 'Platform for the purchase' }, - attributes: { type: 'string', description: 'JSON object of subscriber attributes' }, + presentedOfferingIdentifier: { + type: 'string', + description: 'Identifier of the offering presented to the user', + }, + paymentMode: { + type: 'string', + description: 'Payment mode (pay_as_you_go, pay_up_front, free_trial)', + }, + attributes: { + type: 'string', + description: + 'JSON object of subscriber attributes (used by update_subscriber_attributes and create_purchase)', + }, + introductoryPrice: { type: 'number', description: 'Introductory price for the purchase' }, + updatedAtMs: { + type: 'number', + description: 'Unix epoch ms used by RevenueCat to resolve attribute conflicts', + }, extendByDays: { type: 'number', description: 'Number of days to extend (1-365)' }, - platform: { type: 'string', description: 'Platform filter for offerings' }, + expiryTimeMs: { type: 'number', description: 'Absolute new expiry time in ms since epoch' }, + endTimeMs: { + type: 'number', + description: 'Absolute end time for entitlement in ms since epoch', + }, + platform: { type: 'string', description: 'Platform (X-Platform header)' }, }, outputs: { subscriber: { type: 'json', - description: 'Subscriber object with subscriptions and entitlements', + description: + 'Subscriber object (first_seen, original_app_user_id, original_purchase_date, management_url, subscriptions, entitlements, non_subscriptions)', }, offerings: { type: 'json', - description: 'Array of offerings with packages', + description: 'Array of offerings, each with identifier, description, and packages[]', }, current_offering_id: { type: 'string', description: 'Current offering identifier' }, - metadata: { type: 'json', description: 'Operation metadata' }, + metadata: { + type: 'json', + description: + 'Operation metadata. For get_customer: app_user_id, first_seen, active_entitlements, active_subscriptions. For list_offerings: count, current_offering_id.', + }, deleted: { type: 'boolean', description: 'Whether the subscriber was deleted' }, app_user_id: { type: 'string', description: 'The app user ID' }, updated: { type: 'boolean', description: 'Whether the attributes were updated' }, + customer: { + type: 'json', + description: 'Customer object returned by create_purchase (when present in the response)', + }, }, } diff --git a/apps/sim/tools/revenuecat/create_purchase.ts b/apps/sim/tools/revenuecat/create_purchase.ts index ac259f49833..75b58c1d2a4 100644 --- a/apps/sim/tools/revenuecat/create_purchase.ts +++ b/apps/sim/tools/revenuecat/create_purchase.ts @@ -1,5 +1,11 @@ import type { CreatePurchaseParams, CreatePurchaseResponse } from '@/tools/revenuecat/types' -import { SUBSCRIBER_OUTPUT } from '@/tools/revenuecat/types' +import { + extractCustomer, + extractSubscriber, + SUBSCRIBER_OUTPUT, + shapeSubscriber, + throwIfRevenueCatError, +} from '@/tools/revenuecat/types' import type { ToolConfig } from '@/tools/types' export const revenuecatCreatePurchaseTool: ToolConfig< @@ -29,38 +35,73 @@ export const revenuecatCreatePurchaseTool: ToolConfig< required: true, visibility: 'user-or-llm', description: - 'The receipt token or purchase token from the store (App Store receipt, Google Play purchase token, or Stripe subscription ID)', + 'For iOS, the base64-encoded receipt (or JWSTransaction for StoreKit2); for Android the purchase token; for Amazon the receipt; for Stripe the subscription ID or Checkout Session ID; for Roku the transaction ID; for Paddle the subscription ID or transaction ID', }, productId: { type: 'string', - required: true, + required: false, visibility: 'user-or-llm', - description: 'The product identifier for the purchase', + description: + 'Apple, Google, Amazon, Roku, or Paddle product identifier or SKU. Required for Google.', }, price: { type: 'number', required: false, visibility: 'user-or-llm', - description: 'The price of the product in the currency specified', + description: 'Price of the product. Required if you provide a currency.', }, currency: { type: 'string', required: false, visibility: 'user-or-llm', - description: 'ISO 4217 currency code (e.g., USD, EUR)', + description: 'ISO 4217 currency code (e.g., USD, EUR). Required if you provide a price.', }, isRestore: { type: 'boolean', required: false, visibility: 'user-or-llm', - description: 'Whether this is a restore of a previous purchase', + description: 'Deprecated. Triggers configured restore behavior for shared fetch tokens.', }, - platform: { + presentedOfferingIdentifier: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Identifier of the offering presented to the customer at the time of purchase. Attached to new transactions in this fetch token and exposed in ETL exports and webhooks.', + }, + paymentMode: { type: 'string', required: false, visibility: 'user-or-llm', description: - 'Platform of the purchase (ios, android, amazon, macos, stripe). Required for Stripe and Paddle purchases.', + 'Payment mode for the introductory period. One of: pay_as_you_go, pay_up_front, free_trial. Defaults to free_trial when an introductory period is detected and no value is provided.', + }, + introductoryPrice: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Introductory price paid (if any).', + }, + attributes: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'JSON object of subscriber attributes to set alongside the purchase. Each key maps to {"value": string, "updated_at_ms": number}.', + }, + updatedAtMs: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'UNIX epoch in milliseconds used to resolve attribute conflicts at the request level.', + }, + platform: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Platform of the purchase. One of: ios, android, amazon, macos, uikitformac, stripe, roku, paddle. Sent as the X-Platform header (required by RevenueCat).', }, }, @@ -81,34 +122,46 @@ export const revenuecatCreatePurchaseTool: ToolConfig< const body: Record = { app_user_id: params.appUserId, fetch_token: params.fetchToken, - product_id: params.productId, } + if (params.productId) body.product_id = params.productId if (params.price !== undefined) body.price = params.price if (params.currency) body.currency = params.currency if (params.isRestore !== undefined) body.is_restore = params.isRestore + if (params.presentedOfferingIdentifier) { + body.presented_offering_identifier = params.presentedOfferingIdentifier + } + if (params.paymentMode) body.payment_mode = params.paymentMode + if (params.introductoryPrice !== undefined) { + body.introductory_price = params.introductoryPrice + } + if (params.attributes !== undefined && params.attributes !== '') { + body.attributes = + typeof params.attributes === 'string' ? JSON.parse(params.attributes) : params.attributes + } + if (params.updatedAtMs !== undefined) body.updated_at_ms = params.updatedAtMs return body }, }, transformResponse: async (response) => { + await throwIfRevenueCatError(response) const data = await response.json() - const subscriber = data.subscriber ?? {} - return { success: true, output: { - subscriber: { - first_seen: subscriber.first_seen ?? '', - original_app_user_id: subscriber.original_app_user_id ?? '', - subscriptions: subscriber.subscriptions ?? {}, - entitlements: subscriber.entitlements ?? {}, - non_subscriptions: subscriber.non_subscriptions ?? {}, - }, + customer: extractCustomer(data), + subscriber: shapeSubscriber(extractSubscriber(data)), }, } }, outputs: { + customer: { + type: 'object', + description: + 'Customer object returned at the top level of POST /v1/receipts (first_seen, last_seen, original_app_user_id, original_application_version, original_sdk_version, management_url, entitlements, original_purchase_date, request_date). Null when the response uses the `value`-wrapped envelope.', + optional: true, + }, subscriber: { ...SUBSCRIBER_OUTPUT, description: 'The updated subscriber object after recording the purchase', diff --git a/apps/sim/tools/revenuecat/defer_google_subscription.ts b/apps/sim/tools/revenuecat/defer_google_subscription.ts index 8a2df5aae13..cc744735c2c 100644 --- a/apps/sim/tools/revenuecat/defer_google_subscription.ts +++ b/apps/sim/tools/revenuecat/defer_google_subscription.ts @@ -2,7 +2,12 @@ import type { DeferGoogleSubscriptionParams, DeferGoogleSubscriptionResponse, } from '@/tools/revenuecat/types' -import { SUBSCRIBER_OUTPUT } from '@/tools/revenuecat/types' +import { + extractSubscriber, + SUBSCRIBER_OUTPUT, + shapeSubscriber, + throwIfRevenueCatError, +} from '@/tools/revenuecat/types' import type { ToolConfig } from '@/tools/types' export const revenuecatDeferGoogleSubscriptionTool: ToolConfig< @@ -37,38 +42,59 @@ export const revenuecatDeferGoogleSubscriptionTool: ToolConfig< }, extendByDays: { type: 'number', - required: true, + required: false, + visibility: 'user-or-llm', + description: + 'Number of days to extend the subscription by (1-365). Provide either extendByDays or expiryTimeMs.', + }, + expiryTimeMs: { + type: 'number', + required: false, visibility: 'user-or-llm', - description: 'Number of days to extend the subscription by (1-365)', + description: + 'Absolute new expiry time in milliseconds since Unix epoch. Use instead of extendByDays to set an exact expiry.', }, }, request: { url: (params) => - `https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId)}/subscriptions/${encodeURIComponent(params.productId)}/defer`, + `https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId.trim())}/subscriptions/${encodeURIComponent(params.productId.trim())}/defer`, method: 'POST', headers: (params) => ({ Authorization: `Bearer ${params.apiKey}`, 'Content-Type': 'application/json', }), - body: (params) => ({ - extend_by_days: params.extendByDays, - }), + body: (params) => { + const hasExtend = params.extendByDays !== undefined + const hasExpiry = params.expiryTimeMs !== undefined + if (!hasExtend && !hasExpiry) { + throw new Error('Provide either extendByDays or expiryTimeMs to defer a subscription') + } + if (hasExtend && hasExpiry) { + throw new Error( + 'Provide only one of extendByDays or expiryTimeMs — they cannot be used together' + ) + } + const body: Record = {} + if (hasExpiry) body.expiry_time_ms = params.expiryTimeMs + else if (hasExtend) { + const days = params.extendByDays as number + if (!Number.isFinite(days) || days < 1 || days > 365) { + throw new Error('extendByDays must be an integer between 1 and 365') + } + body.extend_by_days = days + } + return body + }, }, transformResponse: async (response) => { + await throwIfRevenueCatError(response) const data = await response.json() - const subscriber = data.subscriber ?? {} - return { success: true, output: { - subscriber: { - first_seen: subscriber.first_seen ?? '', - original_app_user_id: subscriber.original_app_user_id ?? '', - subscriptions: subscriber.subscriptions ?? {}, - entitlements: subscriber.entitlements ?? {}, - }, + subscriber: shapeSubscriber(extractSubscriber(data)), }, } }, diff --git a/apps/sim/tools/revenuecat/delete_customer.ts b/apps/sim/tools/revenuecat/delete_customer.ts index 017de1ce514..2ff4e1468f7 100644 --- a/apps/sim/tools/revenuecat/delete_customer.ts +++ b/apps/sim/tools/revenuecat/delete_customer.ts @@ -1,5 +1,5 @@ import type { DeleteCustomerParams, DeleteCustomerResponse } from '@/tools/revenuecat/types' -import { DELETE_OUTPUT_PROPERTIES } from '@/tools/revenuecat/types' +import { DELETE_OUTPUT_PROPERTIES, throwIfRevenueCatError } from '@/tools/revenuecat/types' import type { ToolConfig } from '@/tools/types' export const revenuecatDeleteCustomerTool: ToolConfig< @@ -28,7 +28,7 @@ export const revenuecatDeleteCustomerTool: ToolConfig< request: { url: (params) => - `https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId)}`, + `https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId.trim())}`, method: 'DELETE', headers: (params) => ({ Authorization: `Bearer ${params.apiKey}`, @@ -37,11 +37,24 @@ export const revenuecatDeleteCustomerTool: ToolConfig< }, transformResponse: async (response, params) => { + await throwIfRevenueCatError(response) + let body: Record = {} + try { + body = await response.json() + } catch { + // Some delete responses have empty bodies — treat as success + } return { - success: response.ok, + success: true, output: { - deleted: response.ok, - app_user_id: params?.appUserId ?? '', + deleted: + typeof body.deleted === 'boolean' + ? body.deleted + : typeof body.was_deleted === 'boolean' + ? body.was_deleted + : true, + app_user_id: + typeof body.app_user_id === 'string' ? body.app_user_id : (params?.appUserId ?? ''), }, } }, diff --git a/apps/sim/tools/revenuecat/get_customer.ts b/apps/sim/tools/revenuecat/get_customer.ts index 884a47c254b..6989078be1c 100644 --- a/apps/sim/tools/revenuecat/get_customer.ts +++ b/apps/sim/tools/revenuecat/get_customer.ts @@ -1,5 +1,11 @@ import type { CustomerResponse, GetCustomerParams } from '@/tools/revenuecat/types' -import { METADATA_OUTPUT_PROPERTIES, SUBSCRIBER_OUTPUT } from '@/tools/revenuecat/types' +import { + extractSubscriber, + METADATA_OUTPUT_PROPERTIES, + SUBSCRIBER_OUTPUT, + shapeSubscriber, + throwIfRevenueCatError, +} from '@/tools/revenuecat/types' import type { ToolConfig } from '@/tools/types' export const revenuecatGetCustomerTool: ToolConfig = { @@ -25,7 +31,7 @@ export const revenuecatGetCustomerTool: ToolConfig - `https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId)}`, + `https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId.trim())}`, method: 'GET', headers: (params) => ({ Authorization: `Bearer ${params.apiKey}`, @@ -34,31 +40,49 @@ export const revenuecatGetCustomerTool: ToolConfig { + await throwIfRevenueCatError(response) const data = await response.json() - const subscriber = data.subscriber ?? {} - const entitlements = subscriber.entitlements ?? {} - const subscriptions = subscriber.subscriptions ?? {} + const subscriberRaw = extractSubscriber(data) + const subscriber = shapeSubscriber(subscriberRaw) + const requestDate = (data?.value?.request_date ?? data?.request_date) as string | undefined + const now = requestDate ? new Date(requestDate).getTime() : Date.now() - const activeEntitlements = Object.values(entitlements).filter( - (e: unknown) => (e as Record).is_active - ).length - const activeSubscriptions = Object.keys(subscriptions).length + const isActiveByDates = ( + expires: string | null | undefined, + grace: string | null | undefined, + refundedAt?: string | null | undefined + ) => { + if (refundedAt) return false + if (!expires) return true + if (new Date(expires).getTime() > now) return true + if (grace && new Date(grace).getTime() > now) return true + return false + } + + const activeEntitlements = Object.values(subscriber.entitlements).filter((e) => { + const ent = e as Record + return isActiveByDates( + ent.expires_date as string | null | undefined, + ent.grace_period_expires_date as string | null | undefined + ) + }).length + + const activeSubscriptions = Object.values(subscriber.subscriptions).filter((s) => { + const sub = s as Record + return isActiveByDates( + sub.expires_date as string | null | undefined, + sub.grace_period_expires_date as string | null | undefined, + sub.refunded_at as string | null | undefined + ) + }).length return { success: true, output: { - subscriber: { - first_seen: subscriber.first_seen ?? '', - original_app_user_id: subscriber.original_app_user_id ?? '', - original_purchase_date: subscriber.original_purchase_date ?? null, - management_url: subscriber.management_url ?? null, - subscriptions: subscriptions, - entitlements: entitlements, - non_subscriptions: subscriber.non_subscriptions ?? {}, - }, + subscriber, metadata: { - app_user_id: subscriber.original_app_user_id ?? '', - first_seen: subscriber.first_seen ?? '', + app_user_id: subscriber.original_app_user_id, + first_seen: subscriber.first_seen, active_entitlements: activeEntitlements, active_subscriptions: activeSubscriptions, }, diff --git a/apps/sim/tools/revenuecat/grant_entitlement.ts b/apps/sim/tools/revenuecat/grant_entitlement.ts index ad1cb1237a9..d17946b7d8e 100644 --- a/apps/sim/tools/revenuecat/grant_entitlement.ts +++ b/apps/sim/tools/revenuecat/grant_entitlement.ts @@ -1,5 +1,10 @@ import type { GrantEntitlementParams, GrantEntitlementResponse } from '@/tools/revenuecat/types' -import { SUBSCRIBER_OUTPUT } from '@/tools/revenuecat/types' +import { + extractSubscriber, + SUBSCRIBER_OUTPUT, + shapeSubscriber, + throwIfRevenueCatError, +} from '@/tools/revenuecat/types' import type { ToolConfig } from '@/tools/types' export const revenuecatGrantEntitlementTool: ToolConfig< @@ -32,10 +37,17 @@ export const revenuecatGrantEntitlementTool: ToolConfig< }, duration: { type: 'string', - required: true, + required: false, + visibility: 'user-or-llm', + description: + 'Duration of the entitlement. Provide either duration or endTimeMs. One of: daily, three_day, weekly, two_week, monthly, two_month, three_month, six_month, yearly, lifetime', + }, + endTimeMs: { + type: 'number', + required: false, visibility: 'user-or-llm', description: - 'Duration of the entitlement (daily, three_day, weekly, monthly, two_month, three_month, six_month, yearly, lifetime)', + 'Absolute end time in milliseconds since Unix epoch. Use instead of duration to grant the entitlement until a specific timestamp.', }, startTimeMs: { type: 'number', @@ -48,32 +60,31 @@ export const revenuecatGrantEntitlementTool: ToolConfig< request: { url: (params) => - `https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId)}/entitlements/${encodeURIComponent(params.entitlementIdentifier)}/promotional`, + `https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId.trim())}/entitlements/${encodeURIComponent(params.entitlementIdentifier.trim())}/promotional`, method: 'POST', headers: (params) => ({ Authorization: `Bearer ${params.apiKey}`, 'Content-Type': 'application/json', }), body: (params) => { - const body: Record = { duration: params.duration } + if (!params.duration && params.endTimeMs === undefined) { + throw new Error('Provide either duration or endTimeMs to grant a promotional entitlement') + } + const body: Record = {} + if (params.endTimeMs !== undefined) body.end_time_ms = params.endTimeMs + else if (params.duration) body.duration = params.duration if (params.startTimeMs !== undefined) body.start_time_ms = params.startTimeMs return body }, }, transformResponse: async (response) => { + await throwIfRevenueCatError(response) const data = await response.json() - const subscriber = data.subscriber ?? {} - return { success: true, output: { - subscriber: { - first_seen: subscriber.first_seen ?? '', - original_app_user_id: subscriber.original_app_user_id ?? '', - subscriptions: subscriber.subscriptions ?? {}, - entitlements: subscriber.entitlements ?? {}, - }, + subscriber: shapeSubscriber(extractSubscriber(data)), }, } }, diff --git a/apps/sim/tools/revenuecat/list_offerings.ts b/apps/sim/tools/revenuecat/list_offerings.ts index 529c75b8272..f2efd8fdbba 100644 --- a/apps/sim/tools/revenuecat/list_offerings.ts +++ b/apps/sim/tools/revenuecat/list_offerings.ts @@ -2,6 +2,7 @@ import type { ListOfferingsParams, ListOfferingsResponse } from '@/tools/revenue import { OFFERING_OUTPUT_PROPERTIES, OFFERINGS_METADATA_OUTPUT_PROPERTIES, + throwIfRevenueCatError, } from '@/tools/revenuecat/types' import type { ToolConfig } from '@/tools/types' @@ -34,7 +35,7 @@ export const revenuecatListOfferingsTool: ToolConfig - `https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId)}/offerings`, + `https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId.trim())}/offerings`, method: 'GET', headers: (params) => { const headers: Record = { @@ -49,6 +50,7 @@ export const revenuecatListOfferingsTool: ToolConfig { + await throwIfRevenueCatError(response) const data = await response.json() const offerings = data.offerings ?? [] const currentOfferingId = data.current_offering_id ?? null diff --git a/apps/sim/tools/revenuecat/refund_google_subscription.ts b/apps/sim/tools/revenuecat/refund_google_subscription.ts index 603f92efd66..8fa2a03f043 100644 --- a/apps/sim/tools/revenuecat/refund_google_subscription.ts +++ b/apps/sim/tools/revenuecat/refund_google_subscription.ts @@ -2,7 +2,12 @@ import type { RefundGoogleSubscriptionParams, RefundGoogleSubscriptionResponse, } from '@/tools/revenuecat/types' -import { SUBSCRIBER_OUTPUT } from '@/tools/revenuecat/types' +import { + extractSubscriber, + SUBSCRIBER_OUTPUT, + shapeSubscriber, + throwIfRevenueCatError, +} from '@/tools/revenuecat/types' import type { ToolConfig } from '@/tools/types' export const revenuecatRefundGoogleSubscriptionTool: ToolConfig< @@ -11,7 +16,8 @@ export const revenuecatRefundGoogleSubscriptionTool: ToolConfig< > = { id: 'revenuecat_refund_google_subscription', name: 'RevenueCat Refund Google Subscription', - description: 'Refund and optionally revoke a Google Play subscription (Google Play only)', + description: + 'Refund a specific store transaction by its store transaction identifier and revoke access (subscription or non-subscription, last 365 days)', version: '1.0.0', params: { @@ -27,17 +33,18 @@ export const revenuecatRefundGoogleSubscriptionTool: ToolConfig< visibility: 'user-or-llm', description: 'The app user ID of the subscriber', }, - productId: { + storeTransactionId: { type: 'string', required: true, visibility: 'user-or-llm', - description: 'The Google Play product identifier of the subscription to refund', + description: + 'The store transaction identifier of the purchase to refund (e.g., GPA.3309-9122-6177-45730 for Google Play)', }, }, request: { url: (params) => - `https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId)}/subscriptions/${encodeURIComponent(params.productId)}/refund`, + `https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId.trim())}/transactions/${encodeURIComponent(params.storeTransactionId.trim())}/refund`, method: 'POST', headers: (params) => ({ Authorization: `Bearer ${params.apiKey}`, @@ -46,18 +53,12 @@ export const revenuecatRefundGoogleSubscriptionTool: ToolConfig< }, transformResponse: async (response) => { + await throwIfRevenueCatError(response) const data = await response.json() - const subscriber = data.subscriber ?? {} - return { success: true, output: { - subscriber: { - first_seen: subscriber.first_seen ?? '', - original_app_user_id: subscriber.original_app_user_id ?? '', - subscriptions: subscriber.subscriptions ?? {}, - entitlements: subscriber.entitlements ?? {}, - }, + subscriber: shapeSubscriber(extractSubscriber(data)), }, } }, diff --git a/apps/sim/tools/revenuecat/revoke_entitlement.ts b/apps/sim/tools/revenuecat/revoke_entitlement.ts index 5a3c99d0e20..015d4bc7936 100644 --- a/apps/sim/tools/revenuecat/revoke_entitlement.ts +++ b/apps/sim/tools/revenuecat/revoke_entitlement.ts @@ -1,5 +1,10 @@ import type { RevokeEntitlementParams, RevokeEntitlementResponse } from '@/tools/revenuecat/types' -import { SUBSCRIBER_OUTPUT } from '@/tools/revenuecat/types' +import { + extractSubscriber, + SUBSCRIBER_OUTPUT, + shapeSubscriber, + throwIfRevenueCatError, +} from '@/tools/revenuecat/types' import type { ToolConfig } from '@/tools/types' export const revenuecatRevokeEntitlementTool: ToolConfig< @@ -34,7 +39,7 @@ export const revenuecatRevokeEntitlementTool: ToolConfig< request: { url: (params) => - `https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId)}/entitlements/${encodeURIComponent(params.entitlementIdentifier)}/revoke_promotionals`, + `https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId.trim())}/entitlements/${encodeURIComponent(params.entitlementIdentifier.trim())}/revoke_promotionals`, method: 'POST', headers: (params) => ({ Authorization: `Bearer ${params.apiKey}`, @@ -43,18 +48,12 @@ export const revenuecatRevokeEntitlementTool: ToolConfig< }, transformResponse: async (response) => { + await throwIfRevenueCatError(response) const data = await response.json() - const subscriber = data.subscriber ?? {} - return { success: true, output: { - subscriber: { - first_seen: subscriber.first_seen ?? '', - original_app_user_id: subscriber.original_app_user_id ?? '', - subscriptions: subscriber.subscriptions ?? {}, - entitlements: subscriber.entitlements ?? {}, - }, + subscriber: shapeSubscriber(extractSubscriber(data)), }, } }, diff --git a/apps/sim/tools/revenuecat/revoke_google_subscription.ts b/apps/sim/tools/revenuecat/revoke_google_subscription.ts index 68ec3f8e1a8..208ac42e697 100644 --- a/apps/sim/tools/revenuecat/revoke_google_subscription.ts +++ b/apps/sim/tools/revenuecat/revoke_google_subscription.ts @@ -2,7 +2,12 @@ import type { RevokeGoogleSubscriptionParams, RevokeGoogleSubscriptionResponse, } from '@/tools/revenuecat/types' -import { SUBSCRIBER_OUTPUT } from '@/tools/revenuecat/types' +import { + extractSubscriber, + SUBSCRIBER_OUTPUT, + shapeSubscriber, + throwIfRevenueCatError, +} from '@/tools/revenuecat/types' import type { ToolConfig } from '@/tools/types' export const revenuecatRevokeGoogleSubscriptionTool: ToolConfig< @@ -38,7 +43,7 @@ export const revenuecatRevokeGoogleSubscriptionTool: ToolConfig< request: { url: (params) => - `https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId)}/subscriptions/${encodeURIComponent(params.productId)}/revoke`, + `https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId.trim())}/subscriptions/${encodeURIComponent(params.productId.trim())}/revoke`, method: 'POST', headers: (params) => ({ Authorization: `Bearer ${params.apiKey}`, @@ -47,18 +52,12 @@ export const revenuecatRevokeGoogleSubscriptionTool: ToolConfig< }, transformResponse: async (response) => { + await throwIfRevenueCatError(response) const data = await response.json() - const subscriber = data.subscriber ?? {} - return { success: true, output: { - subscriber: { - first_seen: subscriber.first_seen ?? '', - original_app_user_id: subscriber.original_app_user_id ?? '', - subscriptions: subscriber.subscriptions ?? {}, - entitlements: subscriber.entitlements ?? {}, - }, + subscriber: shapeSubscriber(extractSubscriber(data)), }, } }, diff --git a/apps/sim/tools/revenuecat/types.ts b/apps/sim/tools/revenuecat/types.ts index 1d9f0415e8f..4376dacaf2b 100644 --- a/apps/sim/tools/revenuecat/types.ts +++ b/apps/sim/tools/revenuecat/types.ts @@ -76,43 +76,40 @@ export const SUBSCRIPTION_OUTPUT_PROPERTIES = { } as const satisfies Record export const ENTITLEMENT_OUTPUT_PROPERTIES = { - grant_date: { type: 'string', description: 'ISO 8601 grant date', optional: true }, - expires_date: { type: 'string', description: 'ISO 8601 expiration date', optional: true }, - product_identifier: { type: 'string', description: 'Product identifier', optional: true }, - is_active: { type: 'boolean', description: 'Whether the entitlement is active' }, - will_renew: { - type: 'boolean', - description: 'Whether the entitlement will renew', + expires_date: { + type: 'string', + description: 'ISO 8601 expiration date (null for non-expiring entitlements)', optional: true, }, - period_type: { + grace_period_expires_date: { type: 'string', - description: 'Period type (normal, trial, intro, promotional)', + description: 'ISO 8601 grace period expiration date', optional: true, }, + product_identifier: { type: 'string', description: 'Product identifier', optional: true }, purchase_date: { type: 'string', description: 'ISO 8601 date of the latest purchase or renewal', optional: true, }, - store: { +} as const satisfies Record + +export const SUBSCRIBER_OUTPUT_PROPERTIES = { + first_seen: { type: 'string', description: 'ISO 8601 date when subscriber was first seen' }, + last_seen: { type: 'string', - description: 'Store the entitlement was granted from', + description: 'ISO 8601 date when subscriber was last seen', optional: true, }, - grace_period_expires_date: { + original_app_user_id: { type: 'string', description: 'Original app user ID' }, + original_application_version: { type: 'string', - description: 'ISO 8601 grace period expiration date', + description: 'iOS only. First App Store version of your app the customer installed', optional: true, }, -} as const satisfies Record - -export const SUBSCRIBER_OUTPUT_PROPERTIES = { - first_seen: { type: 'string', description: 'ISO 8601 date when subscriber was first seen' }, - original_app_user_id: { type: 'string', description: 'Original app user ID' }, original_purchase_date: { type: 'string', - description: 'ISO 8601 date of original purchase', + description: 'iOS only. Date the app was first purchased/downloaded', optional: true, }, management_url: { @@ -135,6 +132,17 @@ export const SUBSCRIBER_OUTPUT_PROPERTIES = { description: 'Map of non-subscription product identifiers to arrays of purchase objects', optional: true, }, + other_purchases: { + type: 'object', + description: 'Other purchases attached to the subscriber', + optional: true, + }, + subscriber_attributes: { + type: 'object', + description: + 'Custom attributes set on the subscriber. Only returned when using a secret API key', + optional: true, + }, } as const satisfies Record export const SUBSCRIBER_OUTPUT: OutputProperty = { @@ -186,6 +194,51 @@ export const OFFERINGS_METADATA_OUTPUT_PROPERTIES = { }, } as const satisfies Record +/** + * Several RevenueCat v1 endpoints (post receipts, update attributes, revoke promotionals, + * defer/refund/revoke Google subscriptions) wrap responses in `{ value: { request_date, subscriber } }`. + * GET customer info returns the same payload unwrapped. This helper handles both shapes. + */ +export function extractSubscriber(data: unknown): Record { + if (!data || typeof data !== 'object') return {} + const root = data as Record + const wrapped = root.value as Record | undefined + const subscriber = (wrapped?.subscriber ?? root.subscriber) as Record | undefined + return subscriber ?? {} +} + +/** + * POST /v1/receipts may return a top-level `customer` object alongside `subscriber`. + * Returns null when not present (e.g., wrapped envelope responses). + */ +export function extractCustomer(data: unknown): Record | null { + if (!data || typeof data !== 'object') return null + const customer = (data as Record).customer + return customer && typeof customer === 'object' ? (customer as Record) : null +} + +/** + * Parse a RevenueCat REST API error response into a meaningful Error. + * RevenueCat returns `{ code, message }` on 4xx/5xx. + */ +export async function throwIfRevenueCatError(response: Response): Promise { + if (response.ok) return + let message = `RevenueCat API error (${response.status})` + try { + const body = await response.clone().json() + if (body && typeof body === 'object') { + const m = (body as Record).message + const c = (body as Record).code + if (typeof m === 'string' && m.length > 0) { + message = c ? `${m} (code ${c})` : m + } + } + } catch { + // Body not JSON — fall back to status-only message + } + throw new Error(message) +} + /** * Base params interface for RevenueCat API calls */ @@ -204,7 +257,8 @@ export interface DeleteCustomerParams extends RevenueCatBaseParams { export interface GrantEntitlementParams extends RevenueCatBaseParams { appUserId: string entitlementIdentifier: string - duration: string + duration?: string + endTimeMs?: number startTimeMs?: number } @@ -221,11 +275,16 @@ export interface ListOfferingsParams extends RevenueCatBaseParams { export interface CreatePurchaseParams extends RevenueCatBaseParams { appUserId: string fetchToken: string - productId: string + productId?: string price?: number currency?: string isRestore?: boolean - platform?: string + presentedOfferingIdentifier?: string + paymentMode?: string + introductoryPrice?: number + attributes?: string + updatedAtMs?: number + platform: string } export interface UpdateSubscriberAttributesParams extends RevenueCatBaseParams { @@ -236,12 +295,13 @@ export interface UpdateSubscriberAttributesParams extends RevenueCatBaseParams { export interface DeferGoogleSubscriptionParams extends RevenueCatBaseParams { appUserId: string productId: string - extendByDays: number + extendByDays?: number + expiryTimeMs?: number } export interface RefundGoogleSubscriptionParams extends RevenueCatBaseParams { appUserId: string - productId: string + storeTransactionId: string } export interface RevokeGoogleSubscriptionParams extends RevenueCatBaseParams { @@ -249,17 +309,39 @@ export interface RevokeGoogleSubscriptionParams extends RevenueCatBaseParams { productId: string } +export interface RevenueCatSubscriber { + first_seen: string + last_seen: string | null + original_app_user_id: string + original_application_version: string | null + original_purchase_date: string | null + management_url: string | null + subscriptions: Record + entitlements: Record + non_subscriptions: Record + other_purchases: Record + subscriber_attributes: Record | null +} + +export function shapeSubscriber(raw: Record): RevenueCatSubscriber { + return { + first_seen: (raw.first_seen as string) ?? '', + last_seen: (raw.last_seen as string | null) ?? null, + original_app_user_id: (raw.original_app_user_id as string) ?? '', + original_application_version: (raw.original_application_version as string | null) ?? null, + original_purchase_date: (raw.original_purchase_date as string | null) ?? null, + management_url: (raw.management_url as string | null) ?? null, + subscriptions: (raw.subscriptions as Record) ?? {}, + entitlements: (raw.entitlements as Record) ?? {}, + non_subscriptions: (raw.non_subscriptions as Record) ?? {}, + other_purchases: (raw.other_purchases as Record) ?? {}, + subscriber_attributes: (raw.subscriber_attributes as Record | null) ?? null, + } +} + export interface CustomerResponse extends ToolResponse { output: { - subscriber: { - first_seen: string - original_app_user_id: string - original_purchase_date: string | null - management_url: string | null - subscriptions: Record - entitlements: Record - non_subscriptions: Record - } + subscriber: RevenueCatSubscriber metadata: { app_user_id: string first_seen: string @@ -278,23 +360,13 @@ export interface DeleteCustomerResponse extends ToolResponse { export interface GrantEntitlementResponse extends ToolResponse { output: { - subscriber: { - first_seen: string - original_app_user_id: string - subscriptions: Record - entitlements: Record - } + subscriber: RevenueCatSubscriber } } export interface RevokeEntitlementResponse extends ToolResponse { output: { - subscriber: { - first_seen: string - original_app_user_id: string - subscriptions: Record - entitlements: Record - } + subscriber: RevenueCatSubscriber } } @@ -318,13 +390,8 @@ export interface ListOfferingsResponse extends ToolResponse { export interface CreatePurchaseResponse extends ToolResponse { output: { - subscriber: { - first_seen: string - original_app_user_id: string - subscriptions: Record - entitlements: Record - non_subscriptions: Record - } + customer: Record | null + subscriber: RevenueCatSubscriber } } @@ -332,39 +399,25 @@ export interface UpdateSubscriberAttributesResponse extends ToolResponse { output: { updated: boolean app_user_id: string + subscriber: RevenueCatSubscriber } } export interface DeferGoogleSubscriptionResponse extends ToolResponse { output: { - subscriber: { - first_seen: string - original_app_user_id: string - subscriptions: Record - entitlements: Record - } + subscriber: RevenueCatSubscriber } } export interface RefundGoogleSubscriptionResponse extends ToolResponse { output: { - subscriber: { - first_seen: string - original_app_user_id: string - subscriptions: Record - entitlements: Record - } + subscriber: RevenueCatSubscriber } } export interface RevokeGoogleSubscriptionResponse extends ToolResponse { output: { - subscriber: { - first_seen: string - original_app_user_id: string - subscriptions: Record - entitlements: Record - } + subscriber: RevenueCatSubscriber } } diff --git a/apps/sim/tools/revenuecat/update_subscriber_attributes.ts b/apps/sim/tools/revenuecat/update_subscriber_attributes.ts index 5fed414a43e..0603d2b77f4 100644 --- a/apps/sim/tools/revenuecat/update_subscriber_attributes.ts +++ b/apps/sim/tools/revenuecat/update_subscriber_attributes.ts @@ -2,6 +2,12 @@ import type { UpdateSubscriberAttributesParams, UpdateSubscriberAttributesResponse, } from '@/tools/revenuecat/types' +import { + extractSubscriber, + SUBSCRIBER_OUTPUT, + shapeSubscriber, + throwIfRevenueCatError, +} from '@/tools/revenuecat/types' import type { ToolConfig } from '@/tools/types' export const revenuecatUpdateSubscriberAttributesTool: ToolConfig< @@ -32,13 +38,13 @@ export const revenuecatUpdateSubscriberAttributesTool: ToolConfig< required: true, visibility: 'user-or-llm', description: - 'JSON object of attributes to set. Each key maps to an object with a "value" field. Example: {"$email": {"value": "user@example.com"}, "$displayName": {"value": "John"}}', + 'JSON object of attributes to set. Each key maps to an object with "value" (string; null or empty deletes the attribute) and "updated_at_ms" (Unix epoch ms used for conflict resolution — required). Example: {"$email": {"value": "user@example.com", "updated_at_ms": 1709195668093}}', }, }, request: { url: (params) => - `https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId)}/attributes`, + `https://api.revenuecat.com/v1/subscribers/${encodeURIComponent(params.appUserId.trim())}/attributes`, method: 'POST', headers: (params) => ({ Authorization: `Bearer ${params.apiKey}`, @@ -52,11 +58,15 @@ export const revenuecatUpdateSubscriberAttributesTool: ToolConfig< }, transformResponse: async (response, params) => { + await throwIfRevenueCatError(response) + const data = await response.json().catch(() => ({})) + const subscriber = shapeSubscriber(extractSubscriber(data)) return { - success: response.ok, + success: true, output: { - updated: response.ok, - app_user_id: params?.appUserId ?? '', + updated: true, + app_user_id: subscriber.original_app_user_id || (params?.appUserId ?? ''), + subscriber, }, } }, @@ -70,5 +80,9 @@ export const revenuecatUpdateSubscriberAttributesTool: ToolConfig< type: 'string', description: 'The app user ID of the updated subscriber', }, + subscriber: { + ...SUBSCRIBER_OUTPUT, + description: 'The updated subscriber object after applying the attribute changes', + }, }, }