From a9b9594ef20b8704ab182f40f6a9080f71ad4361 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 6 May 2026 22:08:56 -0700 Subject: [PATCH 01/11] fix(revenuecat): align tools and block with REST v1 API spec - Validated all 10 tools and the block against context7 REST v1 docs - Unwrap {value:{subscriber}} envelope across post-receipts, attributes, entitlements, and Google subscription endpoints - Trim entitlement output to documented fields (expires_date, grace_period_expires_date, product_identifier, purchase_date) - Add subscriber output fields: last_seen, original_application_version, other_purchases, subscriber_attributes - create_purchase: productId optional (Google-only required); add introductoryPrice, attributes, updated_at_ms; surface customer + subscriber; X-Platform required; presentedOfferingIdentifier and paymentMode - update_subscriber_attributes: read response and surface subscriber; note required updated_at_ms - defer_google_subscription: enforce XOR(extendByDays, expiryTimeMs) and 1-365 range; expiryTimeMs as one-of alternative - grant_entitlement: duration optional, added endTimeMs (one-of) - refund_google_subscription: corrected endpoint to /transactions/{storeTransactionId}/refund - delete_customer: read 'deleted' field (was 'was_deleted') - list_offerings: corrected X-Platform values - get_customer: count active subscriptions by expiry/refund - Added shared throwIfRevenueCatError helper for {code, message} envelope - Moved type coercions from tools.config.tool to tools.config.params to preserve dynamic refs --- .../docs/content/docs/en/tools/revenuecat.mdx | 166 +++++++++------ apps/sim/blocks/blocks/revenuecat.ts | 196 ++++++++++++++++-- apps/sim/tools/revenuecat/create_purchase.ts | 91 ++++++-- .../revenuecat/defer_google_subscription.ts | 56 +++-- apps/sim/tools/revenuecat/delete_customer.ts | 23 +- apps/sim/tools/revenuecat/get_customer.ts | 64 ++++-- .../sim/tools/revenuecat/grant_entitlement.ts | 37 ++-- apps/sim/tools/revenuecat/list_offerings.ts | 4 +- .../revenuecat/refund_google_subscription.ts | 27 +-- .../tools/revenuecat/revoke_entitlement.ts | 19 +- .../revenuecat/revoke_google_subscription.ts | 19 +- apps/sim/tools/revenuecat/types.ts | 193 ++++++++++------- .../update_subscriber_attributes.ts | 24 ++- 13 files changed, 649 insertions(+), 270 deletions(-) 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/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', + }, }, } From 6c7a18e5f50053f4a9ecfd51d400dd6c06e21a2a Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 6 May 2026 22:10:25 -0700 Subject: [PATCH 02/11] docs --- apps/sim/app/(landing)/integrations/data/integrations.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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." }, { From 85f181abeb0960c35f05446636d6151182432f23 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 6 May 2026 22:17:10 -0700 Subject: [PATCH 03/11] fix(revenuecat): address PR review feedback - create_purchase: wrap JSON.parse(attributes) with try/catch and clear error - update_subscriber_attributes: drop subscriber output (endpoint returns empty body) and guard JSON.parse on attributes - grant_entitlement: throw when both duration and endTimeMs are provided, matching defer_google_subscription behavior --- apps/sim/tools/revenuecat/create_purchase.ts | 11 ++++++-- .../sim/tools/revenuecat/grant_entitlement.ts | 3 ++ apps/sim/tools/revenuecat/types.ts | 1 - .../update_subscriber_attributes.ts | 28 ++++++++----------- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/apps/sim/tools/revenuecat/create_purchase.ts b/apps/sim/tools/revenuecat/create_purchase.ts index 75b58c1d2a4..5794acbc83f 100644 --- a/apps/sim/tools/revenuecat/create_purchase.ts +++ b/apps/sim/tools/revenuecat/create_purchase.ts @@ -135,8 +135,15 @@ export const revenuecatCreatePurchaseTool: ToolConfig< body.introductory_price = params.introductoryPrice } if (params.attributes !== undefined && params.attributes !== '') { - body.attributes = - typeof params.attributes === 'string' ? JSON.parse(params.attributes) : params.attributes + if (typeof params.attributes === 'string') { + try { + body.attributes = JSON.parse(params.attributes) + } catch { + throw new Error('attributes must be a valid JSON object') + } + } else { + body.attributes = params.attributes + } } if (params.updatedAtMs !== undefined) body.updated_at_ms = params.updatedAtMs return body diff --git a/apps/sim/tools/revenuecat/grant_entitlement.ts b/apps/sim/tools/revenuecat/grant_entitlement.ts index d17946b7d8e..e61d5363630 100644 --- a/apps/sim/tools/revenuecat/grant_entitlement.ts +++ b/apps/sim/tools/revenuecat/grant_entitlement.ts @@ -70,6 +70,9 @@ export const revenuecatGrantEntitlementTool: ToolConfig< if (!params.duration && params.endTimeMs === undefined) { throw new Error('Provide either duration or endTimeMs to grant a promotional entitlement') } + if (params.duration && params.endTimeMs !== undefined) { + throw new Error('Provide only one of duration or endTimeMs — they cannot be used together') + } const body: Record = {} if (params.endTimeMs !== undefined) body.end_time_ms = params.endTimeMs else if (params.duration) body.duration = params.duration diff --git a/apps/sim/tools/revenuecat/types.ts b/apps/sim/tools/revenuecat/types.ts index 4376dacaf2b..9f3520450de 100644 --- a/apps/sim/tools/revenuecat/types.ts +++ b/apps/sim/tools/revenuecat/types.ts @@ -399,7 +399,6 @@ export interface UpdateSubscriberAttributesResponse extends ToolResponse { output: { updated: boolean app_user_id: string - subscriber: RevenueCatSubscriber } } diff --git a/apps/sim/tools/revenuecat/update_subscriber_attributes.ts b/apps/sim/tools/revenuecat/update_subscriber_attributes.ts index 0603d2b77f4..83bb0e77786 100644 --- a/apps/sim/tools/revenuecat/update_subscriber_attributes.ts +++ b/apps/sim/tools/revenuecat/update_subscriber_attributes.ts @@ -2,12 +2,7 @@ import type { UpdateSubscriberAttributesParams, UpdateSubscriberAttributesResponse, } from '@/tools/revenuecat/types' -import { - extractSubscriber, - SUBSCRIBER_OUTPUT, - shapeSubscriber, - throwIfRevenueCatError, -} from '@/tools/revenuecat/types' +import { throwIfRevenueCatError } from '@/tools/revenuecat/types' import type { ToolConfig } from '@/tools/types' export const revenuecatUpdateSubscriberAttributesTool: ToolConfig< @@ -51,22 +46,27 @@ export const revenuecatUpdateSubscriberAttributesTool: ToolConfig< 'Content-Type': 'application/json', }), body: (params) => { - const attributes = - typeof params.attributes === 'string' ? JSON.parse(params.attributes) : params.attributes + let attributes: unknown + if (typeof params.attributes === 'string') { + try { + attributes = JSON.parse(params.attributes) + } catch { + throw new Error('attributes must be a valid JSON object') + } + } else { + attributes = params.attributes + } return { attributes } }, }, transformResponse: async (response, params) => { await throwIfRevenueCatError(response) - const data = await response.json().catch(() => ({})) - const subscriber = shapeSubscriber(extractSubscriber(data)) return { success: true, output: { updated: true, - app_user_id: subscriber.original_app_user_id || (params?.appUserId ?? ''), - subscriber, + app_user_id: params?.appUserId ?? '', }, } }, @@ -80,9 +80,5 @@ 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', - }, }, } From 3cefd6f7c4c68880570698a6b0a00359212eeabb Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 6 May 2026 22:24:02 -0700 Subject: [PATCH 04/11] fix(revenuecat): tighten docs after sub-segment validation - delete_customer: drop dead was_deleted fallback (docs specify 'deleted') - grant_entitlement: mark duration + startTimeMs as deprecated, clarify startTimeMs only affects expiration calc (not grant time) - list_offerings: replace vague platform description with documented X-Platform enum (ios, android, amazon, stripe, roku, paddle) --- apps/sim/tools/revenuecat/delete_customer.ts | 7 +------ apps/sim/tools/revenuecat/grant_entitlement.ts | 4 ++-- apps/sim/tools/revenuecat/list_offerings.ts | 3 ++- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/apps/sim/tools/revenuecat/delete_customer.ts b/apps/sim/tools/revenuecat/delete_customer.ts index 2ff4e1468f7..1c032d0f514 100644 --- a/apps/sim/tools/revenuecat/delete_customer.ts +++ b/apps/sim/tools/revenuecat/delete_customer.ts @@ -47,12 +47,7 @@ export const revenuecatDeleteCustomerTool: ToolConfig< return { success: true, output: { - deleted: - typeof body.deleted === 'boolean' - ? body.deleted - : typeof body.was_deleted === 'boolean' - ? body.was_deleted - : true, + deleted: typeof body.deleted === 'boolean' ? body.deleted : true, app_user_id: typeof body.app_user_id === 'string' ? body.app_user_id : (params?.appUserId ?? ''), }, diff --git a/apps/sim/tools/revenuecat/grant_entitlement.ts b/apps/sim/tools/revenuecat/grant_entitlement.ts index e61d5363630..016ce2d95f5 100644 --- a/apps/sim/tools/revenuecat/grant_entitlement.ts +++ b/apps/sim/tools/revenuecat/grant_entitlement.ts @@ -40,7 +40,7 @@ export const revenuecatGrantEntitlementTool: ToolConfig< 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', + 'Deprecated. Duration of the entitlement. Provide either duration or endTimeMs (endTimeMs preferred). One of: daily, three_day, weekly, two_week, monthly, two_month, three_month, six_month, yearly, lifetime', }, endTimeMs: { type: 'number', @@ -54,7 +54,7 @@ export const revenuecatGrantEntitlementTool: ToolConfig< required: false, visibility: 'user-or-llm', description: - 'Optional start time in milliseconds since Unix epoch. Set to a past time to achieve custom durations shorter than daily.', + 'Deprecated. Optional start time in milliseconds since Unix epoch, used with duration to determine expiration. Regardless of value, the entitlement is always granted immediately.', }, }, diff --git a/apps/sim/tools/revenuecat/list_offerings.ts b/apps/sim/tools/revenuecat/list_offerings.ts index f2efd8fdbba..3b6e15c9726 100644 --- a/apps/sim/tools/revenuecat/list_offerings.ts +++ b/apps/sim/tools/revenuecat/list_offerings.ts @@ -29,7 +29,8 @@ export const revenuecatListOfferingsTool: ToolConfig Date: Wed, 6 May 2026 22:27:04 -0700 Subject: [PATCH 05/11] docs --- .../docs/content/docs/en/tools/revenuecat.mdx | 37 ++----------------- 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/apps/docs/content/docs/en/tools/revenuecat.mdx b/apps/docs/content/docs/en/tools/revenuecat.mdx index ebd9814dd88..6c2813db7b7 100644 --- a/apps/docs/content/docs/en/tools/revenuecat.mdx +++ b/apps/docs/content/docs/en/tools/revenuecat.mdx @@ -174,9 +174,9 @@ 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 | 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 | +| `duration` | string | No | Deprecated. Duration of the entitlement. Provide either duration or endTimeMs \(endTimeMs preferred\). 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. | +| `startTimeMs` | number | No | Deprecated. Optional start time in milliseconds since Unix epoch, used with duration to determine expiration. Regardless of value, the entitlement is always granted immediately. | #### Output @@ -272,7 +272,7 @@ List all offerings configured for the project | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | RevenueCat API key | | `appUserId` | string | Yes | An app user ID to retrieve offerings for | -| `platform` | string | No | Platform to filter offerings \(ios, android, stripe, etc.\) | +| `platform` | string | No | X-Platform header value. One of: ios, android, amazon, stripe, roku, paddle. Required when using a legacy public API key; ignored with app-specific API keys. | #### Output @@ -307,37 +307,6 @@ 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` From 8204e89db7d94f288b6e03c343ba8222675f2424 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 6 May 2026 23:47:54 -0700 Subject: [PATCH 06/11] fix(revenuecat): clear duration default when endTimeMs is provided The duration dropdown defaults to 'monthly' so any user filling in the advanced endTimeMs field would otherwise hit the XOR guard. Clear duration in the params mapper so endTimeMs takes precedence. --- apps/sim/blocks/blocks/revenuecat.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/sim/blocks/blocks/revenuecat.ts b/apps/sim/blocks/blocks/revenuecat.ts index 316a4fe8425..cf2814dc475 100644 --- a/apps/sim/blocks/blocks/revenuecat.ts +++ b/apps/sim/blocks/blocks/revenuecat.ts @@ -399,6 +399,7 @@ Return ONLY the numeric timestamp, no text.`, } if (params.endTimeMs !== undefined && params.endTimeMs !== '') { next.endTimeMs = Number(params.endTimeMs) + next.duration = undefined } if (params.expiryTimeMs !== undefined && params.expiryTimeMs !== '') { next.expiryTimeMs = Number(params.expiryTimeMs) From 37e4530f97298e6ebce82a515d8e2c3b08685cf8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 7 May 2026 00:03:25 -0700 Subject: [PATCH 07/11] fix(revenuecat): clear extendByDays default when expiryTimeMs is provided Mirror the duration/endTimeMs fix from 8204e89: when expiryTimeMs is populated, clear extendByDays in the params mapper so the empty-string form value does not trip the XOR guard. Also harden the tool-level XOR checks in defer_google_subscription and grant_entitlement to treat empty strings as undefined for direct (non-block) callers. --- apps/sim/blocks/blocks/revenuecat.ts | 1 + .../tools/revenuecat/defer_google_subscription.ts | 4 ++-- apps/sim/tools/revenuecat/grant_entitlement.ts | 14 +++++++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/sim/blocks/blocks/revenuecat.ts b/apps/sim/blocks/blocks/revenuecat.ts index cf2814dc475..e5c48ad64f3 100644 --- a/apps/sim/blocks/blocks/revenuecat.ts +++ b/apps/sim/blocks/blocks/revenuecat.ts @@ -403,6 +403,7 @@ Return ONLY the numeric timestamp, no text.`, } if (params.expiryTimeMs !== undefined && params.expiryTimeMs !== '') { next.expiryTimeMs = Number(params.expiryTimeMs) + next.extendByDays = undefined } if (params.introductoryPrice !== undefined && params.introductoryPrice !== '') { next.introductoryPrice = Number(params.introductoryPrice) diff --git a/apps/sim/tools/revenuecat/defer_google_subscription.ts b/apps/sim/tools/revenuecat/defer_google_subscription.ts index cc744735c2c..8d23a38d75b 100644 --- a/apps/sim/tools/revenuecat/defer_google_subscription.ts +++ b/apps/sim/tools/revenuecat/defer_google_subscription.ts @@ -65,8 +65,8 @@ export const revenuecatDeferGoogleSubscriptionTool: ToolConfig< 'Content-Type': 'application/json', }), body: (params) => { - const hasExtend = params.extendByDays !== undefined - const hasExpiry = params.expiryTimeMs !== undefined + const hasExtend = params.extendByDays !== undefined && (params.extendByDays as unknown) !== '' + const hasExpiry = params.expiryTimeMs !== undefined && (params.expiryTimeMs as unknown) !== '' if (!hasExtend && !hasExpiry) { throw new Error('Provide either extendByDays or expiryTimeMs to defer a subscription') } diff --git a/apps/sim/tools/revenuecat/grant_entitlement.ts b/apps/sim/tools/revenuecat/grant_entitlement.ts index 016ce2d95f5..ed0d84b6855 100644 --- a/apps/sim/tools/revenuecat/grant_entitlement.ts +++ b/apps/sim/tools/revenuecat/grant_entitlement.ts @@ -67,16 +67,20 @@ export const revenuecatGrantEntitlementTool: ToolConfig< 'Content-Type': 'application/json', }), body: (params) => { - if (!params.duration && params.endTimeMs === undefined) { + const hasEnd = params.endTimeMs !== undefined && (params.endTimeMs as unknown) !== '' + const hasDuration = Boolean(params.duration) + if (!hasDuration && !hasEnd) { throw new Error('Provide either duration or endTimeMs to grant a promotional entitlement') } - if (params.duration && params.endTimeMs !== undefined) { + if (hasDuration && hasEnd) { throw new Error('Provide only one of duration or endTimeMs — they cannot be used together') } 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 + if (hasEnd) body.end_time_ms = params.endTimeMs + else if (hasDuration) body.duration = params.duration + if (params.startTimeMs !== undefined && (params.startTimeMs as unknown) !== '') { + body.start_time_ms = params.startTimeMs + } return body }, }, From d926e0ac7a1a039ed3b3e539ade21621aa2633c3 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 7 May 2026 00:16:40 -0700 Subject: [PATCH 08/11] fix(revenuecat): guard NaN in time-ms mappers and require integer days - Block params mapper: only clear duration/extendByDays when the parsed endTimeMs/expiryTimeMs is finite, so invalid input does not silently discard the user's valid companion default - defer_google_subscription: validate extendByDays as integer (was Number.isFinite), matching the error message --- apps/sim/blocks/blocks/revenuecat.ts | 14 ++++++++++---- .../tools/revenuecat/defer_google_subscription.ts | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/sim/blocks/blocks/revenuecat.ts b/apps/sim/blocks/blocks/revenuecat.ts index e5c48ad64f3..c40d1ea53bf 100644 --- a/apps/sim/blocks/blocks/revenuecat.ts +++ b/apps/sim/blocks/blocks/revenuecat.ts @@ -398,12 +398,18 @@ Return ONLY the numeric timestamp, no text.`, next.startTimeMs = Number(params.startTimeMs) } if (params.endTimeMs !== undefined && params.endTimeMs !== '') { - next.endTimeMs = Number(params.endTimeMs) - next.duration = undefined + const endTimeMs = Number(params.endTimeMs) + if (Number.isFinite(endTimeMs)) { + next.endTimeMs = endTimeMs + next.duration = undefined + } } if (params.expiryTimeMs !== undefined && params.expiryTimeMs !== '') { - next.expiryTimeMs = Number(params.expiryTimeMs) - next.extendByDays = undefined + const expiryTimeMs = Number(params.expiryTimeMs) + if (Number.isFinite(expiryTimeMs)) { + next.expiryTimeMs = expiryTimeMs + next.extendByDays = undefined + } } if (params.introductoryPrice !== undefined && params.introductoryPrice !== '') { next.introductoryPrice = Number(params.introductoryPrice) diff --git a/apps/sim/tools/revenuecat/defer_google_subscription.ts b/apps/sim/tools/revenuecat/defer_google_subscription.ts index 8d23a38d75b..418a5ba58b4 100644 --- a/apps/sim/tools/revenuecat/defer_google_subscription.ts +++ b/apps/sim/tools/revenuecat/defer_google_subscription.ts @@ -79,7 +79,7 @@ export const revenuecatDeferGoogleSubscriptionTool: ToolConfig< 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) { + if (!Number.isInteger(days) || days < 1 || days > 365) { throw new Error('extendByDays must be an integer between 1 and 365') } body.extend_by_days = days From e6495c12c9f8a9ff8b05af51df180c355cf7727a Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 7 May 2026 00:31:49 -0700 Subject: [PATCH 09/11] fix(revenuecat): fall back to Date.now() when request_date is malformed A malformed request_date would parse to NaN, making every entitlement and subscription compare false and silently zero active counts. Fall back to Date.now() when the parsed value is not finite. --- apps/sim/tools/revenuecat/get_customer.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/sim/tools/revenuecat/get_customer.ts b/apps/sim/tools/revenuecat/get_customer.ts index 6989078be1c..23aa29046c3 100644 --- a/apps/sim/tools/revenuecat/get_customer.ts +++ b/apps/sim/tools/revenuecat/get_customer.ts @@ -45,7 +45,8 @@ export const revenuecatGetCustomerTool: ToolConfig Date: Thu, 7 May 2026 00:54:19 -0700 Subject: [PATCH 10/11] fix(revenuecat): unwrap value envelope in list_offerings response --- apps/sim/tools/revenuecat/list_offerings.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/sim/tools/revenuecat/list_offerings.ts b/apps/sim/tools/revenuecat/list_offerings.ts index 3b6e15c9726..afb66eee508 100644 --- a/apps/sim/tools/revenuecat/list_offerings.ts +++ b/apps/sim/tools/revenuecat/list_offerings.ts @@ -52,9 +52,17 @@ export const revenuecatListOfferingsTool: ToolConfig { await throwIfRevenueCatError(response) - const data = await response.json() - const offerings = data.offerings ?? [] - const currentOfferingId = data.current_offering_id ?? null + const raw = await response.json() + /** + * RevenueCat's offerings endpoint may return the payload wrapped in `{ value: { ... } }` + * or unwrapped. Normalize to a single shape. + */ + const data = + raw && typeof raw === 'object' && 'value' in raw && raw.value && typeof raw.value === 'object' + ? (raw.value as Record) + : (raw as Record) + const offerings = (data.offerings as Array>) ?? [] + const currentOfferingId = (data.current_offering_id as string | null) ?? null return { success: true, From 8660b21ccb2e02c1ac4c4f8843077af661b56b2e Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 7 May 2026 09:50:36 -0700 Subject: [PATCH 11/11] improvement(revenuecat): include updated_at_ms in attributes placeholder and wand prompt --- apps/sim/blocks/blocks/revenuecat.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/sim/blocks/blocks/revenuecat.ts b/apps/sim/blocks/blocks/revenuecat.ts index c40d1ea53bf..7b676ddf905 100644 --- a/apps/sim/blocks/blocks/revenuecat.ts +++ b/apps/sim/blocks/blocks/revenuecat.ts @@ -288,7 +288,7 @@ Return ONLY the numeric timestamp, no text.`, id: 'attributes', title: 'Attributes', type: 'long-input', - placeholder: '{"$email": {"value": "user@example.com"}}', + placeholder: '{"$email": {"value": "user@example.com", "updated_at_ms": 1709195668093}}', condition: { field: 'operation', value: ['update_subscriber_attributes', 'create_purchase'], @@ -300,17 +300,17 @@ Return ONLY the numeric timestamp, no text.`, wandConfig: { enabled: true, prompt: `Generate a JSON object of RevenueCat subscriber attributes based on the user's description. -Each attribute key maps to an object with a "value" field. +Each attribute key maps to an object with a "value" field (string) and an "updated_at_ms" field (Unix epoch ms; required by the API for conflict resolution — use the current timestamp unless the user specifies otherwise). Reserved attribute keys start with "$": $email, $displayName, $phoneNumber, $mediaSource, $campaign, $adGroup, $ad, $keyword, $creative, $iterableUserId, $iterableCampaignId, $iterableTemplateId, $onesignalId, $airshipChannelId, $cleverTapId, $firebaseAppInstanceId. Custom attributes use plain keys without "$". Examples: - "set email to john@example.com and name to John" -> - {"$email": {"value": "john@example.com"}, "$displayName": {"value": "John"}} + {"$email": {"value": "john@example.com", "updated_at_ms": 1709195668093}, "$displayName": {"value": "John", "updated_at_ms": 1709195668093}} - "set plan to premium and team to acme" -> - {"plan": {"value": "premium"}, "team": {"value": "acme"}} + {"plan": {"value": "premium", "updated_at_ms": 1709195668093}, "team": {"value": "acme", "updated_at_ms": 1709195668093}} -Return ONLY valid JSON.`, +Return ONLY valid JSON - no explanations, no extra text.`, }, }, {