# Get active subscriptions chart data Source: https://documentation.qonversion.io/api-reference/analytics-charts/get-active-subscriptions-chart-data /api-reference/analytics-api.yaml get /{projectApiKey}/chart/active-subscriptions Retrieve active subscriptions analytics. Product filters and segmentation. filter[user_id] and filter[device_id] are NOT applied. # Get paid subscriptions movement chart data Source: https://documentation.qonversion.io/api-reference/analytics-charts/get-paid-subscriptions-movement-chart-data /api-reference/analytics-api.yaml get /{projectApiKey}/chart/paid-subscriptions-movement Retrieve paid subscriptions movement analytics. Common + product filters only. Segmentation, filter[user_id], and filter[device_id] are NOT applied. # Get proceeds chart data Source: https://documentation.qonversion.io/api-reference/analytics-charts/get-proceeds-chart-data /api-reference/analytics-api.yaml get /{projectApiKey}/chart/proceeds Retrieve proceeds analytics. All filters and segmentation supported. # Get users overview chart data Source: https://documentation.qonversion.io/api-reference/analytics-charts/get-users-overview-chart-data /api-reference/analytics-api.yaml get /{projectApiKey}/chart/users-overview Retrieve users overview analytics. User filters and segmentation, no product filters. # Get analytics card data Source: https://documentation.qonversion.io/api-reference/analytics/get-analytics-card-data /api-reference/rest-api-v4.yaml get /analytics/cards/{card_code} Returns the current value of a scalar dashboard card. Cards bundle several related metrics (e.g. `realtime` returns today's trials, subscriptions, in-app purchases and tracked revenue as a four-row table). Card codes: * `realtime` — today-so-far vs. yesterday counters (`trials_count`, `subscriptions_count`, `inapp_count`, `tracked_revenue`). # Get analytics chart data Source: https://documentation.qonversion.io/api-reference/analytics/get-analytics-chart-data /api-reference/rest-api-v4.yaml get /analytics/charts/{chart_code} Returns time-series data for a chart. `chart_code` selects the metric; pass query parameters to constrain the time range and segmentation. Chart codes (25 total) — see `enum` for the full machine-readable list: - **Revenue**: `proceeds` (net), `sales` (gross), `refunds`, `refund-rate`, `arpu`, `arppu`. - **Recurring revenue**: `mrr`, `arr`, `mrr-movement`, `arr-movement` (new / expansion / contraction / churn). - **Subscriptions**: `active-subscriptions`, `new-subscriptions`, `paid-subscriptions-movement`, `subscriptions-overview`, `subscription-cancellation`. - **Trials**: `free-trials`, `active-trials`, `trials-movement`, `trial-cancellation`, `trial-to-paid`. - **Acquisition / conversion**: `users-overview`, `user-to-trial`, `user-to-paid`. - **Aliases** (kept for backward compatibility): `user-to-trial-conversion` = `user-to-trial`, `subscription-cancellation-rate` = `subscription-cancellation`. Segmentation codes (pass via `segmentation`) and filter attribute codes (pass via `filter[][]`) are discoverable from `GET /v4/analytics/charts/{chart_code}/meta`. # Get analytics chart metadata Source: https://documentation.qonversion.io/api-reference/analytics/get-analytics-chart-metadata /api-reference/rest-api-v4.yaml get /analytics/charts/{chart_code}/meta Returns the chart's display metadata: available filter attributes and their concrete values (pre-scoped to the project), available segmentation dimensions, supported time units, and chart types. Use this to build a UI / discover the `filter[][]` and `segmentation` values for `GET /v4/analytics/charts/{chart_code}`. # Get analytics insights Source: https://documentation.qonversion.io/api-reference/analytics/get-analytics-insights /api-reference/rest-api-v4.yaml get /analytics/insights Returns AI-generated insights for the project over the last `period` days. Responses are cached on the upstream; pass `force=true` to regenerate. The `insights[]` array groups observations by type (`critical`, `positive`, `tip`, `info`) — each item has a short `title`, a longer `body`, and a suggested `action`. # Get cohort analytics data Source: https://documentation.qonversion.io/api-reference/analytics/get-cohort-analytics-data /api-reference/rest-api-v4.yaml get /analytics/cohorts Returns cohort-over-time tables for the project. A cohort is a group of users sharing an acquisition event (see `cohort_definition`). Rows are cohorts, columns are time offsets (see `grouping`). Cells are the `metric` (revenue, subscriptions, payers, ARPU, ARPPU). Use `GET /v4/analytics/cohorts/meta` to discover valid filter attributes and their concrete values for the project. # Get cohort analytics metadata Source: https://documentation.qonversion.io/api-reference/analytics/get-cohort-analytics-metadata /api-reference/rest-api-v4.yaml get /analytics/cohorts/meta Returns the discrete value sets needed to build a cohort query: available `mode`s, `grouping`s, `metric`s, `cohort_definition`s, and filter attributes with their concrete values (pre-scoped to the project). # Get LTV analytics data Source: https://documentation.qonversion.io/api-reference/analytics/get-ltv-analytics-data /api-reference/rest-api-v4.yaml get /analytics/ltv Returns lifetime-value curves for cohorts in the requested range. `series` is a list of time-indexed revenue (or ARPU / ARPPU) points; `segment` + `segments` let you split the curve by an attribute. # Get LTV analytics metadata Source: https://documentation.qonversion.io/api-reference/analytics/get-ltv-analytics-metadata /api-reference/rest-api-v4.yaml get /analytics/ltv/meta Returns available `mode`s, `segmentation` attributes, and project-scoped filter attribute values for the LTV endpoint. # Get LTV trial-conversion analytics data Source: https://documentation.qonversion.io/api-reference/analytics/get-ltv-trial-conversion-analytics-data /api-reference/rest-api-v4.yaml get /analytics/ltv/trial-conversion Returns the trial→paid conversion rate for the requested cohort range — number of trials started, number that converted, and the conversion rate as a decimal (0.0–1.0). Accepts the same filter and `mode`/`revenue_type` shape as the LTV chart endpoint. # Get supported analytics currencies Source: https://documentation.qonversion.io/api-reference/analytics/get-supported-analytics-currencies /api-reference/rest-api-v4.yaml get /analytics/currencies Returns the list of three-letter ISO 4217 currency codes accepted by the `currency` query parameter across analytics endpoints. `USD` is always first and is the implicit default when `currency` is omitted. # Get Apple Ads report Source: https://documentation.qonversion.io/api-reference/apple-ads/get-apple-ads-report /api-reference/rest-api-v4.yaml get /analytics/apple-ads/report Qonversion cohort metrics joined with Apple Ads report data (spend, impressions, taps, installs) for one level: campaigns, ad groups, or keywords. Each row carries a `qonversion` block (cohort outcomes: users, trials, revenue, ARPU/ARPPU), an `apple` block (spend and delivery metrics, including `avg_cpa` — Apple's average cost per tap-through install), and a derived `roas` ratio. **CPA is not CAC.** `apple.avg_cpa` is the cost of an install as Apple reports it. The cost of acquiring a paying user is `windows..cac` (window spend / payers — `spend_closed` in the mature reading, `spend` in the growing one), available only when `windows` is requested. The two differ by the whole install-to-payer funnel; never quote one as the other. **Availability**: available to accounts with Apple Ads enabled; other accounts receive `404`. Contact support to enable Apple Ads. **Data markers**: `apple` is `null` for rows without Apple data; `partial_sources` lists Apple data sources not included (empty when complete); `meta.partial_scope` counts campaigns without Apple data. Qonversion metrics are always complete. **Semantics**: revenue is cohort revenue cumulative to date (only the install date is bounded by `[from, to]`); `roas` is a ratio, not a percentage, and not a closed ratio — revenue keeps accruing while spend is bounded by the period. `meta.semantics` in every response defines each metric. **Reporting basis**: `revenue_type` selects the revenue behind the derived metrics (`roas`, `arpu`, `arppu`, and `windows.*.revenue` / `windows.*.roas`) — `gross` (store-billed, default) or `net` (proceeds after store commission, and after store tax where the store reports it). Rows always carry both `revenue_gross` and `revenue_net`, so `revenue_type` never removes a field; `windows.*.cac` and `windows.*.cost_per_trial` depend on spend only. `currency` converts every monetary value at one midpoint rate for the period (see `meta.currency_conversion`). If no exchange rate is available for the period, values are in USD: `currency` is `USD` and `meta.rate_unavailable_for` holds the requested code. Both `currency` and `revenue_type` are echoed at the top level. **Cohort windows** (optional, `windows=`): pass `windows=d7,d30,lifetime` and each row also carries a `windows` block of per-window cohort metrics (installs, trials, payers, revenue, `roas`, `cac`, `cost_per_trial`). See the `windows` parameter and `meta.windows`. **Cohort window modes** (`windows_mode=`): `growing` is the to-date cohort reading — every install of the period, events up to today, and the row's Apple spend for the period as the denominator; its values change until the period matures. `mature` covers matured install dates only — dates past the window length plus `maturation_buffer_days` — so its ratios over that closed date range are comparable across rows and periods of different ages (`lifetime` can still change as cumulative revenue accrues). Both modes share `V4AnalyticsAppleAdsWindow`; the growing one adds `mode`, `through`, and `spend` and omits `spend_closed`. When `windows_mode` is omitted, the server applies its default mode; send `windows_mode` explicitly when the reading matters. If you omit it, check `meta.windows.mode` (or a window's own `mode: growing`) for the reading served. If `meta.windows.mode` is absent, the response uses the mature reading. **Pagination**: this endpoint uses `limit`/`offset` pagination with a `pagination` block, not cursors. Offsets are stable within a sort order. **Rate limits**: the report and series endpoints are rate limited. A `429` response includes `Retry-After`; wait that long before retrying. # Get the Apple Ads connection status Source: https://documentation.qonversion.io/api-reference/apple-ads/get-the-apple-ads-connection-status /api-reference/rest-api-v4.yaml get /integrations/apple-ads/connection Whether this project's Apple Ads connection is authorized, what it allows, and the next step (`next_action`). Status only: this endpoint never returns a private key, client secret, access or refresh token, authorization code, or OAuth state, and it does not start or complete authorization — use `dashboard_url` to connect or reconnect in the dashboard. **Two independent readiness values.** `reporting` and `campaign_management` are reported separately: a connection can allow reporting without allowing campaign changes. `connected` does not imply campaign-management access — check `campaign_management.can_write`. **Multiple Apple Ads organizations.** `campaign_management.state` is `ready` when at least one organization allows campaign changes; `orgs[]` shows each organization. Each change is authorized against the organization of that campaign. **`legacy_credentials`** is a working connection with an API key; migrating to Apple authorization is recommended. **`verifying`** means the connection is awaiting confirmation from Apple. **`campaign_management.state: unknown`** means campaign-management permissions are not confirmed yet (`reason_code` `permissions_not_checked_yet` or `permission_data_stale`); check again later. **Availability**: available to accounts with Apple Ads enabled; other accounts receive `404`. Contact support to enable Apple Ads. # Get the Apple Ads daily series Source: https://documentation.qonversion.io/api-reference/apple-ads/get-the-apple-ads-daily-series /api-reference/rest-api-v4.yaml get /analytics/apple-ads/report/series One metric of the Apple Ads report, day by day — the chart behind the table. It uses the same project, cohort filters, and reporting basis, in a different shape: no entity axis, no pagination, and no cohort windows. **Not derivable from the report.** The report's cohort metrics are cumulative to date from each install date, while a series point is the value attributed to that day. Use this endpoint for movement over time, and the report for the breakdown by level. **`null` is not zero.** A point is `null` when no value is available for that day — for example, a day outside `coverage`, or a ratio metric with an empty denominator. `0` is a reported zero. **Filters that do not apply** to the requested metric are listed in `filters_ignored`. This includes `filter[status]` and `filter[delivery]`, which do not apply to a daily series. **Length**: a series returns at most 366 daily points. For a longer period, the oldest days are omitted and `truncated` is `true`. **Availability**: available to accounts with Apple Ads enabled; other accounts receive `404`. Contact support to enable Apple Ads. **Rate limits**: the report and series endpoints are rate limited. A `429` response includes `Retry-After`; wait that long before retrying. # Create an automation Source: https://documentation.qonversion.io/api-reference/automations/create-an-automation /api-reference/rest-api-v4.yaml post /automations Deprecated — the Automations product surface has been removed; this API remains available for existing integrations only. Creates a new automation for the authenticated project. # Delete an automation Source: https://documentation.qonversion.io/api-reference/automations/delete-an-automation /api-reference/rest-api-v4.yaml delete /automations/{automation_id} Deprecated — the Automations product surface has been removed; this API remains available for existing integrations only. Permanently deletes an automation by ID. # Get an automation Source: https://documentation.qonversion.io/api-reference/automations/get-an-automation /api-reference/rest-api-v4.yaml get /automations/{automation_id} Deprecated — the Automations product surface has been removed; this API remains available for existing integrations only. Returns a single automation by ID. # List automations Source: https://documentation.qonversion.io/api-reference/automations/list-automations /api-reference/rest-api-v4.yaml get /automations Deprecated — the Automations product surface has been removed; this API remains available for existing integrations only. Returns a paginated list of automations for the authenticated project. Automations are ordered by ID (descending). Supports cursor-based pagination via `limit` and `starting_after`. # Update an automation Source: https://documentation.qonversion.io/api-reference/automations/update-an-automation /api-reference/rest-api-v4.yaml put /automations/{automation_id} Deprecated — the Automations product surface has been removed; this API remains available for existing integrations only. Fully replaces an automation's mutable fields. All mutable fields must be provided. # Update automation status Source: https://documentation.qonversion.io/api-reference/automations/update-automation-status /api-reference/rest-api-v4.yaml patch /automations/{automation_id}/status Deprecated — the Automations product surface has been removed; this API remains available for existing integrations only. Toggles the status of an automation between `active` and `inactive`. # Delete a customer (GDPR) Source: https://documentation.qonversion.io/api-reference/customers/delete-a-customer-gdpr /api-reference/rest-api-v4.yaml delete /customers/{customer_id} Permanently deletes a customer and all associated personal data. This operation is irreversible and intended for GDPR right-to-erasure requests. Returns 204 on success. # Get a customer Source: https://documentation.qonversion.io/api-reference/customers/get-a-customer /api-reference/rest-api-v4.yaml get /customers/{customer_id} Returns a single customer by ID. # Get aggregated customer metrics Source: https://documentation.qonversion.io/api-reference/customers/get-aggregated-customer-metrics /api-reference/rest-api-v4.yaml get /customers/metrics Returns aggregated metrics for customers of the authenticated project. # Get customer metrics (Analytics) Source: https://documentation.qonversion.io/api-reference/customers/get-customer-metrics-analytics /api-reference/analytics-api.yaml get /{projectApiKey}/customers-metrics Retrieve aggregated customer metrics for a project. Accepts the same filters and `search` as the customers list (no `page`, `limit`, or sort). Returns counts (active subscribers, churned subscribers, active trials, billing-retry users), percentages (cancelled-trial rate, churned-subscriber rate), monetary aggregates (total sales, average price), and average payment count. # Get customers list Source: https://documentation.qonversion.io/api-reference/customers/get-customers-list /api-reference/analytics-api.yaml get /{projectApiKey}/customers Retrieve a paginated list of customers with optional filters and search. Example: ```bash curl "https://api.qonversion.io/v1/analytics/JFPATc4VaaWYsfurml3qZ4zsmNw0VfWH/customers?page=1&limit=20&sort_by=since&sort_order=desc&environment=1" ``` **Pagination:** Pages are 1-based at the public boundary. `page=1` returns the first page. Compute `total_pages` from the response as `ceil(total_count / limit)`; the response itself does not contain a `total_pages` field. # Grant a permission to a customer Source: https://documentation.qonversion.io/api-reference/customers/grant-a-permission-to-a-customer /api-reference/rest-api-v4.yaml post /customers/{customer_id}/permissions Grants a permission to the specified customer. Returns 201 with a Location header pointing to the new permission resource. If `expires_at` is provided it must be a valid RFC 3339 / ISO 8601 timestamp. # List customer permissions Source: https://documentation.qonversion.io/api-reference/customers/list-customer-permissions /api-reference/rest-api-v4.yaml get /customers/{customer_id}/permissions Returns a list of all active permissions for the specified customer. # List customers Source: https://documentation.qonversion.io/api-reference/customers/list-customers /api-reference/rest-api-v4.yaml get /customers Returns a paginated list of customers for the authenticated project. Returns a list envelope with ISO 8601 timestamps. Supports cursor-based pagination via `limit` and `starting_after`. # Revoke a customer permission Source: https://documentation.qonversion.io/api-reference/customers/revoke-a-customer-permission /api-reference/rest-api-v4.yaml delete /customers/{customer_id}/permissions/{permission_id} Revokes a specific permission from a customer. Returns 204 on success. Returns 404 if the customer or permission does not exist. # Set customer properties Source: https://documentation.qonversion.io/api-reference/customers/set-customer-properties /api-reference/rest-api-v4.yaml post /customers/{customer_id}/properties Sets one or more custom properties on a customer. Accepts up to 100 key/value pairs per request. Keys must be 1–256 characters; values must be at most 1024 characters. # Create an entitlement definition Source: https://documentation.qonversion.io/api-reference/entitlements/create-an-entitlement-definition /api-reference/rest-api-v4.yaml post /entitlements Creates a new entitlement definition for the authenticated project. # Delete an entitlement definition Source: https://documentation.qonversion.io/api-reference/entitlements/delete-an-entitlement-definition /api-reference/rest-api-v4.yaml delete /entitlements/{entitlement_id} Deletes an entitlement definition by ID. Returns 204 on success. Returns 404 if not found. # Get an entitlement definition Source: https://documentation.qonversion.io/api-reference/entitlements/get-an-entitlement-definition /api-reference/rest-api-v4.yaml get /entitlements/{entitlement_id} Returns a single entitlement definition by ID. # Grant an entitlement Source: https://documentation.qonversion.io/api-reference/entitlements/grant-an-entitlement /api-reference/rest-api.yaml post /users/{user_id}/entitlements Manually grants an entitlement to a user. **Requires Secret Key authentication.** This endpoint requires a **Secret Key** (`sk_` prefix). Never expose it in client-side code. # Grant an entitlement to a user Source: https://documentation.qonversion.io/api-reference/entitlements/grant-an-entitlement-to-a-user /api-reference/rest-api-v4.yaml post /users/{user_id}/entitlements Grants an entitlement definition to a specific user, optionally with an expiry time. Attempting to extend or modify a user entitlement whose source is a paid purchase (store or Stripe) returns `422 paid_entitlement`. # List entitlement definitions Source: https://documentation.qonversion.io/api-reference/entitlements/list-entitlement-definitions /api-reference/rest-api-v4.yaml get /entitlements Returns a paginated list of entitlement definitions for the authenticated project. Entitlements are ordered by creation date (newest first). # List user entitlements Source: https://documentation.qonversion.io/api-reference/entitlements/list-user-entitlements /api-reference/rest-api-v4.yaml get /users/{user_id}/entitlements Returns all entitlements currently granted to the specified user. The response is wrapped in the standard list envelope, but pagination is not supported on this endpoint — `has_more` is always `false` and the full set is returned in a single call. # Retrieve entitlements Source: https://documentation.qonversion.io/api-reference/entitlements/retrieve-entitlements /api-reference/rest-api.yaml get /users/{user_id}/entitlements Retrieves all entitlements for a user. # Revoke a user entitlement Source: https://documentation.qonversion.io/api-reference/entitlements/revoke-a-user-entitlement /api-reference/rest-api-v4.yaml delete /users/{user_id}/entitlements/{entitlement_id} Revokes a previously granted entitlement from a user. Only entitlements whose source is `manual` can be revoked. Returns 204 on success, 404 if the entitlement is not active for the user, and `422 paid_entitlement` when the entitlement came from a paid purchase (store or Stripe). # Revoke an entitlement Source: https://documentation.qonversion.io/api-reference/entitlements/revoke-an-entitlement /api-reference/rest-api.yaml delete /users/{user_id}/entitlements/{id} Revokes a manually granted entitlement from a user. **Requires Secret Key authentication.** You can only revoke entitlements with source `manual`. This endpoint requires a **Secret Key** (`sk_` prefix). Only entitlements with source `manual` can be revoked. # Update an entitlement definition (partial) Source: https://documentation.qonversion.io/api-reference/entitlements/update-an-entitlement-definition-partial /api-reference/rest-api-v4.yaml patch /entitlements/{entitlement_id} # List events Source: https://documentation.qonversion.io/api-reference/events/list-events /api-reference/rest-api-v4.yaml get /events Returns a list of event definitions (track events) for the authenticated project. Events are emitted by the SDK and represent subscription lifecycle and user behavior. # Attach a user to an experiment group Source: https://documentation.qonversion.io/api-reference/experiments/attach-a-user-to-an-experiment-group /api-reference/rest-api-v4.yaml post /experiments/{experiment_id}/users/{user_id} Manually assigns a user to a specific experiment group. The assignment persists; raising traffic later does not re-admit previously rejected users, and finishing the experiment releases all users. It also invalidates the SDK's in-memory config cache. # Change experiment status Source: https://documentation.qonversion.io/api-reference/experiments/change-experiment-status /api-reference/rest-api-v4.yaml post /experiments/{experiment_id}/status Changes the status of an experiment (e.g. start, pause, finish). # Create an experiment Source: https://documentation.qonversion.io/api-reference/experiments/create-an-experiment /api-reference/rest-api-v4.yaml post /experiments Creates a new experiment. Returns 201 with the created experiment and Location header. # Create an experiment group Source: https://documentation.qonversion.io/api-reference/experiments/create-an-experiment-group /api-reference/rest-api-v4.yaml post /experiments/{experiment_id}/groups Creates a new group for the specified experiment. Returns 201 with Location header. # Delete an experiment Source: https://documentation.qonversion.io/api-reference/experiments/delete-an-experiment /api-reference/rest-api-v4.yaml delete /experiments/{experiment_id} Deletes an experiment by ID. Returns 204 on success. # Delete an experiment group Source: https://documentation.qonversion.io/api-reference/experiments/delete-an-experiment-group /api-reference/rest-api-v4.yaml delete /experiments/{experiment_id}/groups/{group_id} Deletes the specified experiment group. Returns 204 on success. # Detach a user from an experiment Source: https://documentation.qonversion.io/api-reference/experiments/detach-a-user-from-an-experiment /api-reference/rest-api-v4.yaml delete /experiments/{experiment_id}/users/{user_id} Removes a user's assignment from an experiment, invalidates the SDK's in-memory config cache, and makes the user eligible for automatic experiment assignment on subsequent requests. # Get an experiment Source: https://documentation.qonversion.io/api-reference/experiments/get-an-experiment /api-reference/rest-api-v4.yaml get /experiments/{experiment_id} Returns a single experiment by ID. # Get experiments analytics summary Source: https://documentation.qonversion.io/api-reference/experiments/get-experiments-analytics-summary /api-reference/rest-api-v4.yaml get /experiments/summary Returns aggregated analytics summary for experiments. # List experiment groups Source: https://documentation.qonversion.io/api-reference/experiments/list-experiment-groups /api-reference/rest-api-v4.yaml get /experiments/{experiment_id}/groups Returns all groups for the specified experiment. # List experiments Source: https://documentation.qonversion.io/api-reference/experiments/list-experiments /api-reference/rest-api-v4.yaml get /experiments Returns all experiments for the authenticated project. # Partially update an experiment Source: https://documentation.qonversion.io/api-reference/experiments/partially-update-an-experiment /api-reference/rest-api-v4.yaml patch /experiments/{experiment_id} Updates specified fields of an experiment. Only provided fields are updated. # Update an experiment group Source: https://documentation.qonversion.io/api-reference/experiments/update-an-experiment-group /api-reference/rest-api-v4.yaml patch /experiments/{experiment_id}/groups/{group_id} Updates fields of the specified experiment group. All fields optional. # Create export (async) Source: https://documentation.qonversion.io/api-reference/exports/create-export-async /api-reference/rest-api-v4.yaml post /exports Creates a new data export task. Returns 202 Accepted with a Location header pointing to the status endpoint. Exports are project-scoped and require the RAW_DATA_ACCESS feature. # Get export status Source: https://documentation.qonversion.io/api-reference/exports/get-export-status /api-reference/rest-api-v4.yaml get /exports/{export_id} Check the status of an export or download the result. # List past exports Source: https://documentation.qonversion.io/api-reference/exports/list-past-exports /api-reference/rest-api-v4.yaml get /exports/history Returns a list of past exports for the authenticated project. # Create an identity link Source: https://documentation.qonversion.io/api-reference/identities/create-an-identity-link /api-reference/rest-api-v4.yaml post /identities Links a Qonversion anonymous user to an external identity ID. If `user_id` is null, a new user is created and linked. This is the mechanism for matching Qonversion anonymous users with your own user IDs. # Get an identity by external ID Source: https://documentation.qonversion.io/api-reference/identities/get-an-identity-by-external-id /api-reference/rest-api-v4.yaml get /identities/{identity_id} Returns the identity linked to the specified external ID. An identity represents the association between a Qonversion anonymous user and an external user identifier from your system. # Create an identity Source: https://documentation.qonversion.io/api-reference/identity/create-an-identity /api-reference/rest-api.yaml post /identities/{identity_id} Creates an identity. If `user_id` is not provided, a new Qonversion user is created automatically. # Retrieve an identity Source: https://documentation.qonversion.io/api-reference/identity/retrieve-an-identity /api-reference/rest-api.yaml get /identities/{identity_id} Retrieves an identity by its ID. Identities map your internal user IDs to Qonversion User IDs, enabling cross-platform user tracking. # Create integration Source: https://documentation.qonversion.io/api-reference/integrations/create-integration /api-reference/rest-api-v4.yaml post /integrations Creates a new integration for the project. # Delete integration Source: https://documentation.qonversion.io/api-reference/integrations/delete-integration /api-reference/rest-api-v4.yaml delete /integrations/{integration_id} Permanently deletes an integration. # Get integrations metadata Source: https://documentation.qonversion.io/api-reference/integrations/get-integrations-metadata /api-reference/rest-api-v4.yaml get /integrations/meta Returns the catalog of supported integration providers grouped by category. Each item exposes its `slug` (use as `integration` in `POST /v4/integrations`) and `allowedStores` (the subset of `target_platform` values the provider supports). Per-provider credential schemas are **not** returned — credentials are configured out-of-band after the integration record is created. # List integrations Source: https://documentation.qonversion.io/api-reference/integrations/list-integrations /api-reference/rest-api-v4.yaml get /integrations Returns every integration configured for the project. There is no cursor or offset pagination: the full set is returned in a single call and `has_more` is always `false`. # Update integration status Source: https://documentation.qonversion.io/api-reference/integrations/update-integration-status /api-reference/rest-api-v4.yaml post /integrations/{integration_id}/status Enables or disables an integration. # Create an offering Source: https://documentation.qonversion.io/api-reference/offerings/create-an-offering /api-reference/rest-api-v4.yaml post /offerings Legacy — Offerings are superseded by Remote Configs; create Remote Configs for new integrations instead. Creates a new offering for the authenticated project. If the project has no offerings yet, the new one is auto-promoted to main (`tag` will be `1` in the response even if it was omitted in the request). Prefer `POST /v4/offerings/{offering_id}/set-main` over passing `tag=1` here when switching the main of an existing project. # Delete an offering Source: https://documentation.qonversion.io/api-reference/offerings/delete-an-offering /api-reference/rest-api-v4.yaml delete /offerings/{offering_id} Legacy — Offerings are superseded by Remote Configs; use Remote Configs for new integrations. Removes the offering and its product attachments. Experiment-variant offerings (owned by the experiments service) cannot be deleted via this API and return `422`. # Get an offering Source: https://documentation.qonversion.io/api-reference/offerings/get-an-offering /api-reference/rest-api-v4.yaml get /offerings/{offering_id} Legacy — Offerings are superseded by Remote Configs; use Remote Configs for new integrations. Returns a single offering by ID. # List offerings Source: https://documentation.qonversion.io/api-reference/offerings/list-offerings /api-reference/rest-api-v4.yaml get /offerings Legacy — Offerings are superseded by Remote Configs; use Remote Configs for new integrations. Returns regular offerings only. Experiment-variant offerings (owned by the experiments service) are excluded. # Set main offering Source: https://documentation.qonversion.io/api-reference/offerings/set-main-offering /api-reference/rest-api-v4.yaml post /offerings/{offering_id}/set-main Legacy — Offerings are superseded by Remote Configs; use Remote Configs for new integrations. Atomically marks the offering as the project's main offering (`tag = 1`) and clears the previous main (`tag = 0`), inside a single transaction. No request body required — the action target is fully identified by the path parameter. Pass an `Idempotency-Key` header to make retries safe (the same key replays the original response). Experiment-variant offerings return `422` (typed code `cannot_setmain_experiment_variant`). # Update an offering Source: https://documentation.qonversion.io/api-reference/offerings/update-an-offering /api-reference/rest-api-v4.yaml patch /offerings/{offering_id} Legacy — Offerings are superseded by Remote Configs; use Remote Configs for new integrations. Partial update. Only supplied fields are changed. `product_ids` replaces the full list in the given order — pass `[]` to detach all products, omit the field to leave the list unchanged. # Create a product Source: https://documentation.qonversion.io/api-reference/products/create-a-product /api-reference/rest-api-v4.yaml post /products Creates a new product for the authenticated project. # Delete a product Source: https://documentation.qonversion.io/api-reference/products/delete-a-product /api-reference/rest-api-v4.yaml delete /products/{product_id} Deletes a product by ID. Returns 204 on success. Returns 404 if the product does not exist. # Get a product Source: https://documentation.qonversion.io/api-reference/products/get-a-product /api-reference/rest-api-v4.yaml get /products/{product_id} Returns a single product by ID. # Get products Source: https://documentation.qonversion.io/api-reference/products/get-products /api-reference/rest-api.yaml get /products/{id} Retrieve a single product by ID, or omit the ID to get all dashboard products. # List products Source: https://documentation.qonversion.io/api-reference/products/list-products /api-reference/rest-api-v4.yaml get /products Returns a paginated list of products for the authenticated project. Products are ordered by creation date (newest first). Manifest-compliant: list envelope, string enums, ISO 8601 timestamps. # Patch a product Source: https://documentation.qonversion.io/api-reference/products/patch-a-product /api-reference/rest-api.yaml patch /products/{uid} Partially updates a product. Only the submitted fields are updated — omitted fields remain unchanged. # Update a product Source: https://documentation.qonversion.io/api-reference/products/update-a-product /api-reference/rest-api.yaml put /products/{uid} Replaces all product fields. Unlike PATCH, all fields must be provided — omitted fields are reset to defaults. # Update a product (partial) Source: https://documentation.qonversion.io/api-reference/products/update-a-product-partial /api-reference/rest-api-v4.yaml patch /products/{product_id} # Get project settings Source: https://documentation.qonversion.io/api-reference/project-settings/get-project-settings /api-reference/rest-api-v4.yaml get /project-settings Returns the settings for the authenticated project, including API keys, proxy URLs, and project name. # Get store configuration Source: https://documentation.qonversion.io/api-reference/project-settings/get-store-configuration /api-reference/rest-api-v4.yaml get /project-settings/stores/{platform} Returns the store configuration for the specified platform. Sensitive fields (private keys, service account credentials) are masked with boolean flags (has_connect_private_key, has_service_account_key, etc.). # Regenerate project secret key Source: https://documentation.qonversion.io/api-reference/project-settings/regenerate-project-secret-key /api-reference/rest-api-v4.yaml post /project-settings/regenerate-secret Deprecated — rotate Secret Keys by overlap instead: create a new Secret Key in the dashboard (Settings → Developer), migrate your consumers, then delete the old key. See https://documentation.qonversion.io/docs/project-keys. Creates a new secret key, deletes the project's oldest secret key immediately, and returns the new value. Requests with the deleted key are normally rejected within about an hour — see https://documentation.qonversion.io/docs/project-keys#what-happens-after-you-delete-a-key. This is a destructive operation. The endpoint does not honor an `Idempotency-Key` header: every call rotates again. Never retry this request automatically. # Update project settings Source: https://documentation.qonversion.io/api-reference/project-settings/update-project-settings /api-reference/rest-api-v4.yaml patch /project-settings Partially updates the project settings. Only provided fields are changed. # Update store configuration Source: https://documentation.qonversion.io/api-reference/project-settings/update-store-configuration /api-reference/rest-api-v4.yaml post /project-settings/stores/{platform} Updates the store configuration for the specified platform. For Apple: if private keys are not provided, existing values are preserved. For Google: if service account key is not provided, existing value is preserved. # Create a purchase Source: https://documentation.qonversion.io/api-reference/purchases/create-a-purchase /api-reference/rest-api.yaml post /users/{user_id}/purchases Records a purchase for a user. Stripe and Paddle are the recommended paths when calling this API directly; App Store and Play Store purchases normally flow through the Qonversion SDK and rarely need to be POSTed manually. Provide exactly one of `stripe_store_data`, `paddle_store_data`, `app_store_data`, or `play_store_data`. Stripe and Paddle are the typical use cases. `app_store_data` and `play_store_data` are accepted for completeness but App Store and Play Store purchases are normally handled automatically by the Qonversion SDK. # List purchases for a user Source: https://documentation.qonversion.io/api-reference/purchases/list-purchases-for-a-user /api-reference/rest-api-v4.yaml get /users/{user_id}/purchases Returns a paginated list of purchases for the specified user. Purchases are ordered by purchase date (newest first). Supports cursor-based pagination and optional platform filtering. # Attach a user to a remote configuration Source: https://documentation.qonversion.io/api-reference/remote-configurations/attach-a-user-to-a-remote-configuration /api-reference/rest-api-v4.yaml post /remote-configurations/{config_id}/users/{user_id} Pins a specific user to this configuration, overriding its targeting rules. The pin is never re-evaluated and survives config edits until removed; it also invalidates the SDK's in-memory config cache. # Change remote configuration status Source: https://documentation.qonversion.io/api-reference/remote-configurations/change-remote-configuration-status /api-reference/rest-api-v4.yaml patch /remote-configurations/{config_id}/status Changes the status of a remote configuration. Allowed transitions: `draft` → `active`, `draft` → `archived`, `active` → `archived`. Archiving is terminal — an archived configuration can only be deleted. Only `active` configurations are served to the SDK. # Create a remote configuration Source: https://documentation.qonversion.io/api-reference/remote-configurations/create-a-remote-configuration /api-reference/rest-api-v4.yaml post /remote-configurations Creates a new remote configuration. New configurations start in `draft` status. Returns 201 with the created configuration and a Location header. # Delete a remote configuration Source: https://documentation.qonversion.io/api-reference/remote-configurations/delete-a-remote-configuration /api-reference/rest-api-v4.yaml delete /remote-configurations/{config_id} Deletes a remote configuration by ID. Returns 204 on success. # Detach a user from a remote configuration Source: https://documentation.qonversion.io/api-reference/remote-configurations/detach-a-user-from-a-remote-configuration /api-reference/rest-api-v4.yaml delete /remote-configurations/{config_id}/users/{user_id} Removes a user's manual pin to this configuration, invalidates the SDK's in-memory config cache, and makes the user eligible for automatic targeting evaluation on the next request. # Get a remote configuration Source: https://documentation.qonversion.io/api-reference/remote-configurations/get-a-remote-configuration /api-reference/rest-api-v4.yaml get /remote-configurations/{config_id} Returns a single remote configuration by ID, including its read-only inline `payload` values (`{}` when unset). # Get the payload mapping Source: https://documentation.qonversion.io/api-reference/remote-configurations/get-the-payload-mapping /api-reference/rest-api-v4.yaml get /remote-configurations/{config_id}/payload-mapping Returns the payload **mapping** — the key → type schema that tells the SDK how to interpret each payload key. This is the schema, not the values: see the payload endpoint for the JSON values delivered to the SDK. # Get the payload values Source: https://documentation.qonversion.io/api-reference/remote-configurations/get-the-payload-values /api-reference/rest-api-v4.yaml get /remote-configurations/{config_id}/payload Returns the payload **values** — the actual JSON object delivered to the SDK for this configuration (`{}` when unset). This is distinct from the payload mapping, which describes the key → type schema. # List remote configurations Source: https://documentation.qonversion.io/api-reference/remote-configurations/list-remote-configurations /api-reference/rest-api-v4.yaml get /remote-configurations Returns a paginated list of remote configurations for the authenticated project. Configurations are ordered by creation date (newest first). List items do not include the inline `payload` values — read a single configuration or the payload endpoint to fetch them. # Replace the payload values Source: https://documentation.qonversion.io/api-reference/remote-configurations/replace-the-payload-values /api-reference/rest-api-v4.yaml put /remote-configurations/{config_id}/payload Replaces the payload **values** delivered to the SDK. Full replace: the supplied `data` object entirely overwrites the stored payload — to change one key, read the current payload, modify it, and send the whole object back. An empty object `{}` clears the payload. Requests are limited to 64 KiB. # Set the payload mapping Source: https://documentation.qonversion.io/api-reference/remote-configurations/set-the-payload-mapping /api-reference/rest-api-v4.yaml post /remote-configurations/{config_id}/payload-mapping Replaces the payload **mapping** (key → type schema). Full replace: the supplied `data` object becomes the complete mapping. Allowed types: `String`, `Number`, `Bool`, `Json`, `Color`, `Products`, `Screens`. # Update a remote configuration (full replace) Source: https://documentation.qonversion.io/api-reference/remote-configurations/update-a-remote-configuration-full-replace /api-reference/rest-api-v4.yaml put /remote-configurations/{config_id} Replaces the configuration's editable fields. `name` is required; omitted optional fields are reset. Use the status and payload endpoints to change status or payload values. # Create scheduled report Source: https://documentation.qonversion.io/api-reference/scheduled-reports/create-scheduled-report /api-reference/rest-api-v4.yaml post /scheduled-reports Creates a new scheduled report. # Delete scheduled report Source: https://documentation.qonversion.io/api-reference/scheduled-reports/delete-scheduled-report /api-reference/rest-api-v4.yaml delete /scheduled-reports/{report_id} Deletes a scheduled report. # Get scheduled report Source: https://documentation.qonversion.io/api-reference/scheduled-reports/get-scheduled-report /api-reference/rest-api-v4.yaml get /scheduled-reports/{report_id} Returns details for a single scheduled report. # List available destinations Source: https://documentation.qonversion.io/api-reference/scheduled-reports/list-available-destinations /api-reference/rest-api-v4.yaml get /scheduled-reports/destinations Returns all available report destinations (email, S3, GCS, etc.). # List scheduled reports Source: https://documentation.qonversion.io/api-reference/scheduled-reports/list-scheduled-reports /api-reference/rest-api-v4.yaml get /scheduled-reports Returns all scheduled reports for the authenticated project. # Send test report Source: https://documentation.qonversion.io/api-reference/scheduled-reports/send-test-report /api-reference/rest-api-v4.yaml post /scheduled-reports/{report_id}/send-test Triggers a test send for a scheduled report. Returns 202 Accepted. # Update scheduled report Source: https://documentation.qonversion.io/api-reference/scheduled-reports/update-scheduled-report /api-reference/rest-api-v4.yaml put /scheduled-reports/{report_id} Partially update a scheduled report. Omit any field to keep its current value; at least one field must be supplied. # Create a screen Source: https://documentation.qonversion.io/api-reference/screens/create-a-screen /api-reference/rest-api-v4.yaml post /screens Deprecated — the Screens product surface has been removed; this API remains available for existing integrations only. Creates a new screen skeleton for the authenticated project. The created screen has no visual content yet — it is a placeholder you then open in the Qonversion dashboard to lay out components, upload media, and add localisations. Render-time fields (`background`, `default_lang`, `configs`, `content`, `prod_key`, `sandbox_key`) are populated by the dashboard editor, not by this endpoint. Returns `201 Created` with the newly created `V4Screen` and a `Location` header pointing at `GET /v4/screens/{screen_id}`. # Delete a screen Source: https://documentation.qonversion.io/api-reference/screens/delete-a-screen /api-reference/rest-api-v4.yaml delete /screens/{screen_id} Deprecated — the Screens product surface has been removed; this API remains available for existing integrations only. Permanently deletes a screen by ID. # Duplicate a screen Source: https://documentation.qonversion.io/api-reference/screens/duplicate-a-screen /api-reference/rest-api-v4.yaml post /screens/{screen_id}/copy Deprecated — the Screens product surface has been removed; this API remains available for existing integrations only. Creates a copy (duplicate) of an existing screen. The duplicated screen is returned with a new `id` and status `draft`. No request body is required. # Get a screen Source: https://documentation.qonversion.io/api-reference/screens/get-a-screen /api-reference/rest-api-v4.yaml get /screens/{screen_id} Deprecated — the Screens product surface has been removed; this API remains available for existing integrations only. Returns a single screen by ID. By default the response is the full `V4Screen`, including the render-time fields (`background`, `default_lang`, `configs`, `content`, `prod_key`, `sandbox_key`, `used`) an SDK needs to display a paywall. Pass `render=false` to receive the lean `V4ScreenSummary` instead — the same shape returned on `GET /v4/screens`. Useful when you only need metadata and want to avoid fetching the render payload, which can be several kilobytes per screen. # Get per-screen analytics Source: https://documentation.qonversion.io/api-reference/screens/get-per-screen-analytics /api-reference/rest-api-v4.yaml get /screens/{screen_id}/analytics Deprecated — the Screens product surface has been removed; this API remains available for existing integrations only. Returns analytics metrics for a single screen. Supports optional time-range, environment, currency, and unit filters. # Get screens analytics overview Source: https://documentation.qonversion.io/api-reference/screens/get-screens-analytics-overview /api-reference/rest-api-v4.yaml get /screens/analytics/overview Deprecated — the Screens product surface has been removed; this API remains available for existing integrations only. Returns aggregated analytics metrics across all screens for the authenticated project. Supports optional time-range, environment, currency, and unit filters. # List screens Source: https://documentation.qonversion.io/api-reference/screens/list-screens /api-reference/rest-api-v4.yaml get /screens Deprecated — the Screens product surface has been removed; this API remains available for existing integrations only. Returns a paginated list of screens for the authenticated project. Screens are ordered by creation date (newest first) and include every screen visible in the Qonversion dashboard (statuses `draft`, `published`, `modified`, and historical `legacy`). Supports cursor-based pagination via `limit` and `starting_after`. # Publish a screen Source: https://documentation.qonversion.io/api-reference/screens/publish-a-screen /api-reference/rest-api-v4.yaml post /screens/{screen_id}/publish Deprecated — the Screens product surface has been removed; this API remains available for existing integrations only. Publishes a screen, setting its status to `published`. No request body is required. # Update a screen Source: https://documentation.qonversion.io/api-reference/screens/update-a-screen /api-reference/rest-api-v4.yaml put /screens/{screen_id} Deprecated — the Screens product surface has been removed; this API remains available for existing integrations only. Updates a screen's name. All mutable fields must be provided. # Create a segment Source: https://documentation.qonversion.io/api-reference/segments/create-a-segment /api-reference/rest-api-v4.yaml post /segments Creates a new segment. Returns 201 with the created segment and Location header. # Delete a segment Source: https://documentation.qonversion.io/api-reference/segments/delete-a-segment /api-reference/rest-api-v4.yaml delete /segments/{segment_id} Deletes a segment by ID. Returns 204 on success. System segments cannot be deleted (422). # Get a segment Source: https://documentation.qonversion.io/api-reference/segments/get-a-segment /api-reference/rest-api-v4.yaml get /segments/{segment_id} Returns a single segment by ID. # List segments Source: https://documentation.qonversion.io/api-reference/segments/list-segments /api-reference/rest-api-v4.yaml get /segments Returns a paginated list of segments for the authenticated project. Segments are ordered by creation date (newest first). # List system segments Source: https://documentation.qonversion.io/api-reference/segments/list-system-segments /api-reference/rest-api-v4.yaml get /segments/system Returns all predefined system segments. System segments are read-only and cannot be modified or deleted. # Update a segment (full replace) Source: https://documentation.qonversion.io/api-reference/segments/update-a-segment-full-replace /api-reference/rest-api-v4.yaml put /segments/{segment_id} Replaces all fields of a segment. All fields are required. # Create user properties Source: https://documentation.qonversion.io/api-reference/user-properties/create-user-properties /api-reference/rest-api.yaml post /users/{id}/properties Sets properties for a user. Supports partial success - returns both saved properties and errors. **Rate limit:** Properties endpoints are rate-limited per project (default 30 requests per second; configurable on the server). Exceeding the limit returns 429. # List properties for a user Source: https://documentation.qonversion.io/api-reference/user-properties/list-properties-for-a-user /api-reference/rest-api-v4.yaml get /users/{user_id}/properties Returns all stored properties for the specified user (up to 100 per user). The response is always a single page — there is no cursor pagination, so `has_more` is always `false` and `next_cursor` is always `null`. Qonversion-defined keys are prefixed with `_q_` (e.g., `_q_email`). # Retrieve user properties Source: https://documentation.qonversion.io/api-reference/user-properties/retrieve-user-properties /api-reference/rest-api.yaml get /users/{id}/properties Retrieves all properties for a given user. Up to 100 properties per user. **Rate limit:** Properties endpoints are rate-limited per project (default 30 requests per second; configurable on the server). Exceeding the limit returns 429. Qonversion-defined properties are prefixed with `_q_` (e.g. `_q_email`). Custom properties use your own keys. # Set (upsert) properties for a user Source: https://documentation.qonversion.io/api-reference/user-properties/set-upsert-properties-for-a-user /api-reference/rest-api-v4.yaml post /users/{user_id}/properties Upserts one or more properties for the specified user (1-100 items per call). Each property is identified by a key; existing values are overwritten. Supports partial success: as long as **at least one** property passes validation, the response is 200 with successfully saved properties under `saved_properties` and per-key failures under `property_errors`. **Effective validation** (applied per-item): - `key` — 1-80 characters, matching `^[-a-zA-Z0-9_.:]+$` and containing at least one letter. Keys starting with `_` are reserved for Qonversion system properties; only pre-registered keys prefixed with `_q_` (e.g. `_q_email`, `_q_name`) are accepted. - `value` — up to 120 bytes, must not contain `\n`, `\r`, `"`, or `'`. **400 behaviour** — returned without a per-key breakdown when: - the request itself is malformed (empty array, more than 100 items, or key/value exceeding the gateway's 256/1024-character limits), or - every property in the request fails validation (there is nothing to save). In that case retry with the offending keys removed to see per-key reasons in the 200 response. # Create a user Source: https://documentation.qonversion.io/api-reference/users/create-a-user /api-reference/rest-api-v4.yaml post /users Create a new Qonversion user. The server generates the `QON_…` user id and returns it in the response body and the `Location` header. v3's `POST /v3/users/{id}` accepted a client-supplied id; v4 standardises on server-issued ids across every resource. Pass an `Idempotency-Key` header to make retries safe — the same key returns the originally-created user instead of creating a duplicate. # Get a user Source: https://documentation.qonversion.io/api-reference/users/get-a-user /api-reference/rest-api-v4.yaml get /users/{user_id} Returns a single user by ID. # Retrieve a user Source: https://documentation.qonversion.io/api-reference/users/retrieve-a-user /api-reference/rest-api.yaml get /users/{id} Retrieves a Qonversion user by their ID. User IDs are generated by the Qonversion SDK (prefixed with `QON_`) or set manually via the API. # Changelog Source: https://documentation.qonversion.io/changelog Latest updates and improvements to the Qonversion platform ## Apple Ads: Connect with Apple Connect Apple Ads in **Settings → Apple Ads** with **Connect with Apple** — no API key to create or upload. The [integration guide](docs/apple-search-ads) and the new [Apple Ads report guide](docs/apple-ads-report) cover setup, Growing and Mature cohort windows, Gross and Net revenue, and comparing figures with the Apple Ads console. ## Webhook Payload: `period_type` Every webhook event now carries a top-level `period_type` naming the phase the event's transaction belongs to: `trial`, `intro`, `promotional` or `regular`. It describes the transaction the event reports, while the `entitlements` block keeps describing the access the user holds at the moment the event was built. The App Store is the only store that reports all four values, and `promotional` is App Store only; Google Play adds `intro` for the minority of transactions where Google reports an introductory phase; Stripe and Paddle send only `trial` or `regular`. Existing fields are unchanged. The webhooks page now also documents `transaction.promo_offer_id`, which was already being sent, and the exact values the `entitlements` block can carry — including the three that our public API spells differently. See [Webhooks](webhooks#2-request-format). ## Webhook Payload: `id` Every webhook event now carries an `id`, a stable UUID of the event. Every retry and every resend of the same delivery repeats it, so use it as your idempotency key instead of building one out of the transaction fields. Existing fields are unchanged. See [Webhooks](webhooks#2-request-format). ## Multiple Secret Keys and zero-downtime rotation A project can hold up to **10 Secret Keys** at once, managed in the dashboard under **Settings → Developer → Keys**. To rotate, create a new key, move your servers to it, then delete the old one — the old key stops working within 1 hour of deletion, and the new one works immediately. `POST /v4/project-settings/regenerate-secret` and the MCP tool `regenerate_project_secret` are now **deprecated**: they delete the project's oldest Secret Key immediately, with no overlap — which, on a project with several keys, may be a key another server still uses. The earlier docs claim of a "short grace period (seconds)" was wrong — a deleted or regenerated key can keep authenticating for up to 1 hour. See [How to manage and rotate project keys](project-keys) for the full runbook. ## Remote Config Cache Invalidation API New SDK method `invalidateRemoteConfigsCache()` forces the next `remoteConfig` / `remoteConfigList` call to fetch a fresh targeting evaluation instead of serving the in-memory cache — useful right after setting user properties your targeting depends on. Additionally, `identify` now drops the config cache even when the user id doesn't change, so re-login flows always evaluate fresh. Available in **iOS 6.14.0**, **Android 9.7.0**, **React Native 10.11.0**, **Flutter 11.10.0**, **Cordova 7.9.0**, **Capacitor 1.8.0**, and **Unity 9.9.0**. See [Invalidate the config cache](remote-config#invalidate-the-config-cache) for usage and [When a device fetches a config](remote-config-targeting#when-a-device-fetches-a-config) for the full caching rules. ## Freeze Assignments for Remote Configs Keep users on the Remote Config they already received — no matter how the targeting changes later. Turn on **Freeze assignments** in the configuration's Targeting card, and every user who newly receives the config is pinned: later targeting-rule or priority changes won't re-seat them. When enabling, you can optionally pin everyone who has already received the config in one shot. Payload edits still apply to pinned users, and turning the switch off does not unfreeze anyone — detach users individually or archive/delete the configuration to release them. See [Freezing assignments](remote-config-targeting#freezing-assignments) for the exact semantics. ## Web Funnel Sell your subscriptions on the web and unlock them in your app. Build a checkout funnel as a No-Code screen, hosted by Qonversion at `screens.qonversion.io`, let users pay with **Stripe** in the browser (from ads, emails, or influencer links), and have them **redeem** the purchase inside your mobile app — Qonversion grants entitlements and tracks the revenue alongside your App Store and Google Play subscriptions. Available now in **beta / early access**, and you can validate the whole flow end to end with **sandbox testing** before going live. See the [Web Funnel guide](web-funnel) and [Web Funnel redemption](web-funnel-redemption) to get started. ## No-Code Builder & SDK Updates Several No-Code improvements shipped with **iOS SDK 6.13.0** and **Android No-Codes SDK 1.10.0**: * **Custom purchase loader.** Replace the SDK's default purchase spinner with your own — a styled spinner or a **Lottie** animation — configured entirely in the No-Code Builder. See the [Purchase Loader guide](purchase-loader). * **Read a screen's configured products and variables.** Load a screen up front with `loadScreen` and read the product IDs and screen variables configured in the Builder — each value keeping its native type — for analytics consistency and app-side logic. See [Reading default variables](displaying-no-codes#reading-default-variables). * **Custom actions.** Trigger a custom action from a No-Code screen and handle it in your app through a new delegate event. See [Custom Actions](custom-actions). * **No-Codes in Kids Mode.** No-Code screens are now available from the No-IDFA (Kids Mode) Qonversion SDK, starting with **iOS SDK 6.12.1**. See [Kids Mode](kids-mode-sdk). ## Paddle Integration Qonversion now supports [Paddle](paddle-integration) as a web payment provider, alongside the App Store, Google Play, and Stripe. Connect your Paddle account with an API key (Qonversion registers the webhook destination automatically), map your Paddle products, and link purchases to Qonversion users with `custom_data` so entitlements are granted and revenue is tracked from Paddle webhooks. See the [Paddle Integration guide](paddle-integration) and [Paddle Credentials](paddle-credentials) to get started. ## Improved Billing Grace Period Support Subscription analytics charts — including MRR, ARR, MRR Movement, ARR Movement, Active Subscriptions, Subscriptions Movement, and Cohorts — now accurately reflect store-configured billing grace periods for both Apple and Google Play subscriptions. Qonversion now reads the actual grace period duration from App Store Connect and Google Play Console, ensuring that subscriptions in billing retry are counted as active for the correct duration before being classified as churned. No action is required — the improvement is applied automatically. ## Cordova SDK 7.x and Capacitor SDK 1.x New major versions for Cordova and Capacitor SDKs with improved plugin architecture. See the [Cordova 7 migration guide](jan-2026-migration-guide-cordova-7) and [Capacitor 1 migration guide](jan-2026-migration-guide-capacitor-1). ## iOS SDK 6.0 Major update to the iOS SDK with performance improvements and new APIs. See the [iOS 6.0 migration guide](nov-2025-migration-guide-ios-600). ## React Native SDK 10.x Updated React Native SDK with improved TypeScript support and new features. See the [React Native 10 migration guide](sep-2025-migration-guide-react-native-10). # Account Overview Source: https://documentation.qonversion.io/docs/account-overview A single page that aggregates analytics across all projects in your organization. Account Overview page Account Overview gives you a high-level view of revenue, subscriptions, trials, and users without switching between projects one-by-one. ## Who can use it Account Overview is available when: * Your account has **2 or more projects** * Your role grants the **Access to analytics** permission If you don't see the option, contact your account Owner or Administrator to request access. ## How to open it Click the project switcher in the top-left corner of the dashboard. At the bottom of the dropdown you'll see an **Account Overview** button — click it to open the page. While you're on Account Overview, the project switcher shows **All projects** instead of a single project name, and most sidebar items are disabled. To go back to a single project, pick it from the switcher. ## What's on the page ### Top cards Five summary cards show totals for the selected period across all your projects: * **Sales** — gross revenue from purchase events * **MRR** — monthly recurring revenue (active regular subscriptions) * **New Trials** — number of trials started * **New Subscriptions** — number of subscriptions activated * **Active Subscriptions** — number of currently active subscriptions Each card shows the current value and the percentage change vs. the previous period. ### Charts Six time-series charts mirror the cards plus **New Users** (clients created in the period). Hover any chart to see daily values; toggle the legend to focus on specific series. ### Breakdown table (Projects) When you switch **Group By** away from "Total", a per-project breakdown table appears below the charts. It lists each project with the same metrics, so you can spot which projects drive the totals. Click any row to drill into that project's standard analytics. ## Filters and grouping The toolbar above the cards offers two controls: ### Group by Splits charts and the breakdown table by a dimension: * **Total** — single aggregated series (default) * **Project** — one series per project * **Store**, **Country**, **Currency**, **Locale**, **Device**, **OS version**, **SDK version** — split by client property ### Filter Narrows down the data without changing the grouping. Click **Filter**, pick a dimension (Store, Country, Device, etc.), then choose values. Multiple filters apply together (AND). Filter values are loaded from data observed across your projects, so what you see depends on what your apps report. ## Date range and currency * **Date range** — the standard 7d / 30d / 90d / custom controls in the top-right. The previous period (used for the % change indicator) is the same length immediately before the selected range. * **Currency** — change the display currency from the gear icon (top-right). All revenue values (Sales, MRR) are converted using daily exchange rates. ## Tips * **Open one project quickly** — click any row in the breakdown table to jump straight to that project's monitoring dashboard. * **Keep your filters** — date range, group-by, and value filters are preserved in the URL, so you can bookmark or share a specific view. * **Compare apps** — pick "Group by Project" with a 30-day range to see which apps grew and which declined in the period. * **Check trial-to-paid health** — combine **New Trials** and **New Subscriptions** charts; a widening gap may indicate trial conversion drops. # Migrating from Adapty to Qonversion Source: https://documentation.qonversion.io/docs/adapty-migration-guide We have prepared this guide to help you move your infrastructure seamlessly from Adapty to Qonversion. Although this guide is comprehensive and should enable you to transfer the entire infrastructure smoothly, feel free to reach out to support if you have any questions. 1. [Configure dashboard](#configure-dashboard) 2. [Install SDKs](#install-sdks) 3. [User-base migration](#user-base-migration) ## Configure dashboard First and foremost, [register](https://go.qonversion.io/8C4r) at Qonversion. Registration will take you through an onboarding process, where you'll have to fill out information about your project and insert links — all to help you add your project to Qonversion. We highly recommend following this flow and not skipping any settings during registration. We'll also cover the scenario if you don't fill these out. ### Store Settings First, let's set up your Stores in the Qonversion dashboard. To do this, gather the necessary data from Adapty. #### Apple App Store settings To configure the Apple App Store in Qonversion, you will need the following fields: 1. **App-Specific Shared Secret**: Copy this value from the App Store Connect shared secret field in the App Settings -> iOS SDK section in the Adapty dashboard. [Here's](ios-app-store-info-fields#2-generate-the-app-specific-shared-secret) a more detailed guide on how to obtain this. 2. **App Store ID**: You'll find your App Store ID in the URL of your App Store Connect account. For example, if your URL is something like `https://apps.apple.com/us/app/example-app/id123456789`, then 123456789 is your app’s Apple ID. #### Google Play Console Settings Here's what you have at Adapty: To set up your Google Play account in Qonversion, you will need the following: 1. **Android Package Name**: This is the applicationID found in your app-level build.gradle file. You can either copy it from there or use the **Package Name** from your Adapty settings. 2. **Service Account Credentials JSON**: Copy the value from the **Service account key file** shown in the screenshot above, or attach the same file you used with Adapty. [Here's more](android-store-setup) on how you can get Google Play Service Account Key. ### Configure Stores in Qonversion Once you have all the necessary data from Adapty, follow the steps to fill in the required Stores in Qonversion [Stores Settings](https://demo.arcade.software/Eq6Y1v6okCVCUhOcYfUF?embed\&embed_mobile=inline\&embed_desktop=inline\&show_copy_link=true) ### Products, Entitlements and Remote Configs After you've filled the store data, it's time to set up Remote Configs, Products, and Entitlements. Here's another way to describe these elements: | Adapty | Qonversion | | - | - | | Access levels | Entitlements | | Products | Products | | Paywalls | Remote Configs | #### Entitlements (Access levels) Let's start with Entitlements. Similar to how you set up Access levels in Adapty, you need to set up Entitlements in Qonversion. Here’s how it was done in Adapty: Here’s how to do it in Qonversion: Navigate to the Entitlements and click the **Create Entitlement** button. [Create entitlements](https://demo.arcade.software/dYOIEanZRhOmQ9zidazH?embed\&embed_mobile=inline\&embed_desktop=inline\&show_copy_link=true) #### Products Next, you need to create Products similar to how you created in Adapty: To create Products in Qonversion, go to **Entitlements → Products** and click **Create Product**. [Entitlements – Products](https://demo.arcade.software/Evi7fa9nT4xmjzuavh4j?embed\&embed_mobile=inline\&embed_desktop=inline\&show_copy_link=true) * **Qonversion Product ID** is your unique product identifier in Qonversion that corresponds to a unique product on the Apple App Store and Google Play Store. Qonversion SDK will use it to make purchases. * **AppStore Product ID** – product identifier on Apple App Store. Learn more on how to create [subscriptions on iOS](https://qonversion.io/blog/configure-iap-app-store-connect/). * **Google Play Product ID** – product identifier on Google Play Console. Learn more on how to set up [Android in-app products ](android-in-app-products). * **Google Play Base Plan ID** - identifier of the base plan for Google Play Product. This is used for subscription products. If you're creating an in-app, leave this field empty for in-apps. In Qonversion, you will set up different products for the different base plans of the same product. * **Associated Entitlements** - choose the entitlements that should be granted once this product is purchased. #### Remote Configs (instead of Adapty Paywalls) In Adapty, paywalls define which products are presented to the user. In Qonversion, this is covered by [Remote Configs](remote-config): create a configuration with a context key (e.g. `main_paywall`) and put your product IDs into the JSON payload in the order you want to display them: ```json theme={null} { "products": ["weekly_premium", "annual_premium_trial"] } ``` Beyond changing products without an app release, Remote Configs also let you [target user segments](remote-config#5-segment-users) and [run A/B tests](launch-test-from-remote-config). See [the payload convention and code samples](migrate-offerings-to-remote-configs) for details. ### Server-to-server notifications To seamlessly implement server-to-server notifications, use Adapty S2S URLs as a temporary proxy in our system. This allows Qonversion to process new requests while still sending notifications to your existing users. #### Configure Apple App Store 1. Navigate to your Qonversion [project settings](https://dash.qonversion.io/project-settings/stores?tab=ios). Copy the server-to-server notification URL for Apple App Store. 2. Sign in to [App Store Connect](https://appstoreconnect.apple.com/apps) and select your app. 3. Navigate to the App Information section. 4. Copy the Adapty URL you used for server-to-server notifications and replace it with the URL from step 1. 5. Set up the Adapty URL as a Proxy URL in Qonversion in [project settings](https://dash.qonversion.io/project-settings/stores?tab=ios). See more information [here](ios-s2s-notifications). #### Google Developer Notifications 1. Complete the [Android Store setup](android-store-setup), including **Save & verify** for the service account key. 2. Under **Real-time notifications (RTDN)** in [project settings](https://dash.qonversion.io/project-settings/stores?tab=android), check for a Pub/Sub topic. If none is shown, open [Google Play store setup](https://dash.qonversion.io/no-codes/stores/android) for the same project and click **Connect to Google**. Return to project settings and copy the topic. 3. In your app's Google Play Console, open **Monetization setup → Real-time developer notifications**, enter the topic, click **Save changes**, then **Send Test Notification** to check notification delivery. See more information [here](google-developer-notifications). ## Install SDKs Check out the links below for the SDK you need and follow the steps provided: → [iOS SDK](ios-sdk-setup) → [Android SDK](android-sdk) → [Flutter SDK](flutter-sdk) → [React Native SDK](react-native-sdk) → [Unity SDK](unity-sdk) → [Cordova Plugin](cordova) → [Capacitor Plugin](capacitor) → [Web](web-sdk) → [Kids Mode Qonversion SDKs](kids-mode-sdk) Read more about [Installing the SDKs](install-sdk). When you're done with the setup, it's time to get into the code! ### Launch SDK Firstly, you need to initialize the SDK. Here's how you did it in Adapty: ```swift Swift theme={null} func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let configurationBuilder = Adapty.Configuration .Builder(withAPIKey: "PUBLIC_SDK_KEY") .with(observerMode: false) Adapty.activate(with: configurationBuilder) { error in // handle the error } } ``` ```java Java theme={null} public class App extends Application { @Override public void onCreate() { super.onCreate() Adapty.activate( applicationContext, new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withObserverMode(false) .build() ); } } ``` ```kotlin Kotlin theme={null} class App : Application() { override fun onCreate() { super.onCreate() Adapty.activate( applicationContext, AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withObserverMode(false) .build() } } ``` ```dart Flutter theme={null} // Few different steps with iOS Info.plist and AndroidManifest.xml files. // That can be removed while migrating to Qonversion. // And then call: try { Adapty().activate(); } on AdaptyError catch (adaptyError) {} } catch (e) {} ``` ```typescript React Native theme={null} adapty.activate('PUBLIC_SDK_KEY', { observerMode: false }); ``` ```csharp Unity theme={null} // Few different steps with iOS Info.plist and AndroidManifest.xml files. // Create a script which will be responsible for listening of Adapty events. // Few other steps ``` Here's how you'll do it for Qonversion — request the Remote Config holding your product IDs, then resolve them to store products: ```swift Swift theme={null} Qonversion.shared().remoteConfig(contextKey: "main_paywall") { remoteConfig, error in guard let payload = remoteConfig?.payload, let productIds = payload["products"] as? [String] else { return } Qonversion.shared().products { allProducts, error in let paywallProducts = productIds.compactMap { allProducts[$0] } // Display paywallProducts in the configured order } } ``` ```kotlin Kotlin theme={null} Qonversion.shared.remoteConfig("main_paywall", object : QonversionRemoteConfigCallback { override fun onSuccess(remoteConfig: QRemoteConfig) { val productIds = (remoteConfig.payload["products"] as? List<*>) ?.filterIsInstance() ?: return Qonversion.shared.products(object : QonversionProductsCallback { override fun onSuccess(products: Map) { val paywallProducts = productIds.mapNotNull { products[it] } // Display paywallProducts in the configured order } override fun onError(error: QonversionError) { /* handle error */ } }) } override fun onError(error: QonversionError) { /* handle error */ } }) ``` ```dart Flutter theme={null} final remoteConfig = await Qonversion.getSharedInstance().remoteConfig(contextKey: 'main_paywall'); final productIds = List.from(remoteConfig.payload['products'] as List); final products = await Qonversion.getSharedInstance().products(); final paywallProducts = productIds .map((id) => products[id]) .whereType() .toList(); // Display paywallProducts in the configured order ``` ```typescript React Native theme={null} const remoteConfig = await Qonversion.getSharedInstance().remoteConfig('main_paywall'); const productIds: string[] = remoteConfig.payload['products'] ?? []; const products = await Qonversion.getSharedInstance().products(); const paywallProducts = productIds .map((id) => products.get(id)) .filter((product) => product != null); // Display paywallProducts in the configured order ``` ```csharp Unity theme={null} Qonversion.GetSharedInstance().RemoteConfig("main_paywall", (remoteConfig, error) => { if (error != null) return; var productIds = remoteConfig.Payload["products"] as List; Qonversion.GetSharedInstance().Products((products, productsError) => { if (productsError != null) return; // Pick products by the IDs from the payload and display them }); }); ``` [See the full guide: products via Remote Configs](migrate-offerings-to-remote-configs) ### Making purchases Adapty: ```swift Swift theme={null} Adapty.makePurchase(product: product) { result in switch result { case let .success(info): if info.profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // successful purchase } case let .failure(error): // handle the error } } ``` ```kotlin Kotlin theme={null} Adapty.makePurchase(activity, product) { result -> when (result) { is AdaptyResult.Success -> { val info = result.value //NOTE: info is null in case of cross-grade with DEFERRED proration mode val profile = info?.profile if (profile?.accessLevels?.get("YOUR_ACCESS_LEVEL")?.isActive == true) { // grant access to premium features } } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java Java theme={null} Adapty.makePurchase(activity, product, result -> { if (result instanceof AdaptyResult.Success) { AdaptyPurchasedInfo info = ((AdaptyResult.Success) result).getValue(); //NOTE: info is null in case of cross-grade with DEFERRED proration mode AdaptyProfile profile = info != null ? info.getProfile() : null; if (profile != null) { AdaptyProfile.AccessLevel premium = profile.getAccessLevels().get("YOUR_ACCESS_LEVEL"); if (premium != null && premium.isActive()) { // successful purchase } } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` ```dart Flutter theme={null} try { final profile = await Adapty().makePurchase(product: product); if (profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive ?? false) { // successful purchase } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ```csharp Unity theme={null} Adapty.MakePurchase(product, (profile, error) => { if(error != null) { // handle error return; } var accessLevel = profile.AccessLevels["YOUR_ACCESS_LEVEL"]; if (accessLevel != null && accessLevel.IsActive) { // grant access to features } }); ``` ```typescript React Native (TS) theme={null} try { const profile = await adapty.makePurchase(product); const isSubscribed = profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // grant access to features in accordance with access level } } catch (error) { // handle the error } ``` Qonversion: ```swift Swift theme={null} Qonversion.shared().purchase(product) { (result) in if result.isSuccessful { if let premium: Qonversion.Entitlement = result.entitlements["premium"], premium.isActive { // Grant user access to premium features } } else if result.isCanceledByUser { // Handle canceled purchase } else if result.isPending { // Handle pending purchase } else { // Handle errors } } ``` ```objectivec Objective-C theme={null} [[Qonversion sharedInstance] purchaseWithResult:product completion:^(QONPurchaseResult * _Nonnull result) { if (result.isSuccessful) { QONEntitlement *premiumEntitlement = result.entitlements[@"premium"]; if (premiumEntitlement && premiumEntitlement.isActive) { // Grant user access to premium features } } else if (result.isCanceledByUser) { // Handle canceled purchase } else if (result.isPending) { // Handle pending purchase } else { // Handle errors } }]; ``` ```java Java theme={null} Qonversion.getSharedInstance().purchase(this, product, new QonversionPurchaseCallback() { @Override public void onResult(@NonNull QPurchaseResult result) { if (result.isSuccessful()) { QEntitlement premium = result.getEntitlements().get("premium"); if (premium != null && premium.isActive()) { // Grant user access to premium features } } else if (result.isCanceledByUser()) { // Handle canceled purchase } else if (result.isPending()) { // Handle pending purchase } else { // Handle errors } } }); ``` ```kotlin Kotlin theme={null} Qonversion.shared.purchase(this, product, object : QonversionPurchaseCallback { override fun onResult(result: QPurchaseResult) { when { result.isSuccessful -> { val premium = result.entitlements["premium"] if (premium != null && premium.isActive) { // Grant user access to premium features } } result.isCanceledByUser -> { // Handle canceled purchase } result.isPending -> { // Handle pending purchase } else -> { // Handle errors } } } }) ``` ```dart Flutter theme={null} final result = await Qonversion.getSharedInstance().purchaseWithResult(product); if (result.isSuccess) { final premium = result.entitlements?['premium']; if (premium != null && premium.isActive) { // Grant user access to premium features } } else if (result.isCanceled) { // Handle canceled purchase } else if (result.isPending) { // Handle pending purchase } else { // Handle errors } ``` ```typescript React Native theme={null} const result: PurchaseResult = await Qonversion.getSharedInstance().purchaseWithResult(product); if (result.isSuccess) { const premium = result.entitlements?.get('premium'); if (premium && premium.isActive) { // Grant user access to premium features } } else if (result.isCanceled) { // Handle canceled purchase } else if (result.isPending) { // Handle pending purchase } else { // Handle errors } ``` ```csharp Unity theme={null} Qonversion.GetSharedInstance().Purchase(product, (result) => { if (result.IsSuccess) { if (result.Entitlements != null && result.Entitlements.TryGetValue("premium", out var premium) && premium.IsActive) { // Grant user access to premium features } } else if (result.IsCanceled) { // Handle canceled purchase } else if (result.IsPending) { // Handle pending purchase } else { // Handle errors } }); ``` ```typescript Cordova theme={null} const result = await Qonversion.getSharedInstance().purchase(product); if (result.isSuccess) { const premium = result.entitlements?.get('premium'); if (premium && premium.isActive) { // Grant user access to premium features } } else if (result.isCanceled) { // Handle canceled purchase } else if (result.isPending) { // Handle pending purchase } else { // Handle errors } ``` ```typescript Capacitor theme={null} const result = await Qonversion.getSharedInstance().purchase(product); if (result.isSuccess) { const premium = result.entitlements?.get('premium'); if (premium && premium.isActive) { // Grant user access to premium features } } else if (result.isCanceled) { // Handle canceled purchase } else if (result.isPending) { // Handle pending purchase } else { // Handle errors } ``` ### Restore purchases If your app has a restore purchases feature on paywalls, just use the Qonversion `restore()` function when users want to restore their purchases. ```swift Swift theme={null} Qonversion.shared().restore { [weak self] (entitlements, error) in if let error = error { // Handle error } if let entitlement: Qonversion.Entitlement = entitlements["plus"], entitlement.isActive { // Restored and entitlement is active } ``` ```objectivec Objective-C theme={null} [[Qonversion sharedInstance] restore:^(NSDictionary * _Nonnull result, NSError * _Nullable error) { if (error) { // Handle error } QONEntitlement *entitlement = result[@"active"]; if (entitlement && entitlement.isActive) { // Restored and entitlement is active } }]; ``` ```java Java theme={null} Qonversion.getSharedInstance().restore(new QonversionEntitlementsCallback() { @Override public void onSuccess(@NotNull Map entitlements) { QEntitlement premiumEntitlement = entitlements.get("premium"); if (premiumEntitlement != null && premiumEntitlement.isActive()) { // handle active entitlement here } } @Override public void onError(@NotNull QonversionError error) { // handle error here } }); ``` ```kotlin Kotlin theme={null} Qonversion.shared.restore(object : QonversionEntitlementsCallback { override fun onSuccess(entitlements: Map) { val premiumEntitlement = entitlements["premium"] if (premiumEntitlement != null && premiumEntitlement.isActive) { // handle active entitlement here } } override fun onError(error: QonversionError) { // handle error here } }) ``` ```dart Flutter theme={null} try { final Map entitlements = await Qonversion.getSharedInstance().restore(); } catch (e) { print(e); } ``` ```typescript React Native theme={null} try { const entitlements: Map = await Qonversion.getSharedInstance().restore(); } catch (e) { console.log(e); } ``` ```csharp Unity theme={null} Qonversion.GetSharedInstance().Restore((entitlements, error) => { if (error == null) { // Handle entitlements here } else { // Handle the error Debug.Log("Error" + error.ToString()); } }); ``` ```typescript Cordova theme={null} try { const entitlements = await Qonversion.getSharedInstance().restore(); } catch (e) { console.log(e); } ``` ```typescript Capacitor theme={null} try { const entitlements: Map = await Qonversion.getSharedInstance().restore(); } catch (e) { console.log(e); } ``` ### User status To check the customer status in Qonversion, you need to use Entitlements. In Adapty, you used profile and access levels. Adapty: ```swift Swift theme={null} Adapty.getProfile { result in if let profile = try? result.get() { // check the access profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } } ``` ```kotlin Kotlin theme={null} Adapty.getProfile { result -> when (result) { is AdaptyResult.Success -> { val profile = result.value // check the access } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java Java theme={null} Adapty.getProfile(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success) result).getValue(); // check the access } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` ```dart Flutter theme={null} try { final profile = await Adapty().getProfile(); // check the access } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ```csharp Unity theme={null} Adapty.GetProfile((profile, error) => { if (error != null) { // handle the error return; } // check the access }); ``` ```typescript React Native (TS) theme={null} try { const profile = await adapty.getProfile(); } catch (error) { // handle the error } ``` Qonversion: ```swift Swift theme={null} Qonversion.shared().checkEntitlements { (entitlements, error) in if let error = error { // handle error return } if let premium: Qonversion.Entitlement = entitlements["premium"], premium.isActive { // unlock feature } } ``` ```objectivec Objective-C theme={null} [[Qonversion sharedInstance] checkEntitlements:^(NSDictionary * _Nonnull entitlements, NSError * _Nullable error) { QONEntitlement *premiumEntitlement = entitlements[@"premium"]; if (premiumEntitlement && premiumEntitlement.isActive) { // unlock feature } }]; ``` ```java Java theme={null} Qonversion.getSharedInstance().checkEntitlements(new QonversionEntitlementsCallback() { @Override public void onSuccess(@NotNull Map entitlements) { final QEntitlement premiumEntitlement = entitlements.get("premium"); if (premiumEntitlement != null && premiumEntitlement.isActive()) { // unlock feature } } @Override public void onError(@NotNull QonversionError error) { // handle error here } }); ``` ```kotlin Kotlin theme={null} Qonversion.shared.checkEntitlements(object: QonversionEntitlementsCallback { override fun onSuccess(entitlements: Map) { val premiumEntitlement = entitlements["premium"] if (premiumEntitlement != null && premiumEntitlement.isActive) { // unlock feature } } override fun onError(error: QonversionError) { // handle error here } }) ``` ```dart Flutter theme={null} try { final Map entitlements = await Qonversion.getSharedInstance().checkEntitlements(); final premium = entitlements['premium']; if (premium != null && premium.isActive) { // unlock feature } } catch (e) { print(e); } ``` ```typescript React Native theme={null} try { const entitlements = await Qonversion.getSharedInstance().checkEntitlements(); const premiumEntitlement = entitlements.get('premium'); if (premiumEntitlement != null) { // unlock feature } } catch (e) { // handle error here } ``` ```csharp Unity theme={null} Qonversion.GetSharedInstance().CheckEntitlements((entitlements, error) => { if (error == null) { if (entitlements.TryGetValue("premium", out Entitlement premium) && premium.IsActive) { // unlock feature } } else { // Handle the error Debug.Log("Error" + error.ToString()); } }); ``` ```typescript Cordova theme={null} try { const entitlements = await Qonversion.getSharedInstance().checkEntitlements(); const premiumEntitlement = entitlements.get('premium'); if (premiumEntitlement != null) { // unlock feature } } catch (e) { // handle error here } ``` ```typescript Capacitor theme={null} try { const entitlements = await Qonversion.getSharedInstance().checkEntitlements(); const premiumEntitlement = entitlements.get('premium'); if (premiumEntitlement != null) { // unlock feature } } catch (e) { // handle error here } ``` ### Authorization system ### Cross-device and cross-platform access Set up this section only if you need cross-device and cross-platform access and your project includes an authorization system. Qonversion lets you identify your signed-in users and unlock premium access across multiple devices. Use the `identify()` method to link a user to your signed-in subscriber. Call this method every time you want to use User Identity. For example, when a user logs in. User Identity provides a convenient way of managing premium access of your existing subscribers, including the following cases: * A user reinstalls your app for any reason. Using the same User ID allows you to provide premium access linked to previously purchased products. * A user logs in on several devices. You can provide premium access based on a subscription purchased on one of his devices. * A user logs in on iOS and Android versions of your app. You can provide premium access based on a subscription purchased on one of the platforms. #### Logging in When a user logs into his account, call `identify()`. ```swift Swift theme={null} Qonversion.shared().identify("your_custom_user_id") // or the following option, if you want to get notified about the result. Qonversion.shared().identify("your_custom_user_id") { user, error in // use user if necessary } ``` ```objectivec Objective-C theme={null} [[Qonversion sharedInstance] identify:@"your_custom_user_id"]; // or the following option, if you want to get notified about the result. [[Qonversion sharedInstance] identify:@"your_custom_user_id" completion:^(QONUser * _Nullable user, NSError * _Nullable error) { // use user if necessary }]; ``` ```java Java theme={null} Qonversion.getSharedInstance().identify("your_custom_user_id"); // or the following option, if you want to get notified about the result. Qonversion.getSharedInstance().identify("your_custom_user_id", new QonversionUserCallback() { @Override public void onSuccess(@NonNull QUser user) { // use user if necessary } @Override public void onError(@NonNull QonversionError error) { // handle error here } }); ``` ```kotlin Kotlin theme={null} Qonversion.shared.identify("your_custom_user_id") // or the following option, if you want to get notified about the result. Qonversion.shared.identify("your_custom_user_id", object : QonversionUserCallback { override fun onSuccess(user: QUser) { // use user if necessary } override fun onError(error: QonversionError) { // handle error here } }) ``` ```dart Flutter theme={null} try { final userInfo = await Qonversion.getSharedInstance().identify("your_custom_user_id"); // use userInfo if necessary } catch (e) { // handle error here } ``` ```typescript React Native theme={null} try { const userInfo = await Qonversion.getSharedInstance().identify('your_custom_user_id'); // use userInfo if necessary } catch (e) { // handle error here } ``` ```csharp Unity theme={null} Qonversion.GetSharedInstance().Identify("your_custom_user_id") // or the following option, if you want to get notified about the result. Qonversion.GetSharedInstance().Identify("your_custom_user_id", (userInfo, error) => { if (error == null) { // use userInfo if necessary } else { // Handle the error Debug.Log("Error" + error.ToString()); } }); ``` ```typescript Cordova theme={null} try { const userInfo = await Qonversion.getSharedInstance().identify('your_custom_user_id'); // use userInfo if necessary } catch (e) { // handle error here } ``` ```typescript Capacitor theme={null} try { const userInfo = await Qonversion.getSharedInstance().identify('your_custom_user_id'); // use userInfo if necessary } catch (e) { // handle error here } ``` ```bash curl theme={null} curl --location --request POST 'https://api.qonversion.io/v3/identities/new_identities_user' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data-raw '{ "user_id": "QON_38a2d811afd54a433587620f8696266e" }' { "id": "new_identities_user", "user_id": "QON_38a2d811afd54a433587620f8696266e" } ``` ### Use the unique user ID stored in your database Always use unique user ID values. Otherwise, a user can get matched to another user's entitlements status. To check entitlements for identified users, you can call the `checkEntitlements` function, which we discussed above. #### Logging out You need to call the `logout()` method to handle entitlements for an unauthorized user. Call this method when a user logs out within your app: ```swift Swift theme={null} Qonversion.shared().logout() ``` ```objectivec Objective-C theme={null} [[Qonversion sharedInstance] logout]; ``` ```java Java theme={null} Qonversion.getSharedInstance().logout(); ``` ```kotlin Kotlin theme={null} Qonversion.shared.logout() ``` ```dart Flutter theme={null} Qonversion.getSharedInstance().logout(); ``` ```typescript React Native theme={null} Qonversion.getSharedInstance().logout(); ``` ```csharp Unity theme={null} Qonversion.GetSharedInstance().Logout(); ``` ```typescript Cordova theme={null} Qonversion.getSharedInstance().logout(); ``` ```typescript Capacitor theme={null} Qonversion.getSharedInstance().logout(); ``` When a user logs back into his account, don't forget to use `identify()` method again. ## User-base migration Those steps have to be done to keep the system working smoothly for your current user base. The necessity of steps depends on the complexity of your system. The simplest system is represented by a single platform infrastructure (Apple, Google, or Stripe). The complex one includes a few platforms with authorization system, your backend infrastructure, and WebHooks handler. ### Qonversion Android SDK 9.+ limitation For Qonversion Android SDK 9.+, the method `syncHistoricalData()` has been removed because of a limitation in Google Play Billing Library 8, which prevents the retrieval of historical purchases ### Client-side migration Follow these steps to sync user status and retrieve the latest data from Google and Apple on your current device. Call the `syncHistoricalData()` method right after Qonversion SDK initialization to synchronize all the device-related data and ensure that none of the entitlements has been missed. ### User-database migration Once you have the app version with Qonversion SDK up and running, it's time to proceed to the server-side migration. 1. Prepare files with [AppStore with Base64 encoded receipt data](migrating-subscriptions#app-store-data-migration-file) or [Google Play with purchase tokens](migrating-subscriptions#google-play-data-migration-file). If you do not have receipt data or purchase tokens on your side but are already running your subscription management with a third-party vendor, you should ask the vendor’s support team for the required data file. 2. Then simply **share the files with us** using our support chat and provide CSV files. ## Release After finishing all the steps, you're ready to release your app. For complex systems with cross-platform access and authorization setups, make sure your Qonversion team has verified the user database migration. ### You're good to go Check out the detailed Adapty and Qonversion [feature comparison](https://qonversion.io/adapty-alternative) and don't hesitate to contact us if you have questions. *** [Migrating from RevenueCat](migrating-from-revenuecat-to-qonversion) [Users and Access](users-and-access) # Adjust Source: https://documentation.qonversion.io/docs/adjust Send iOS and Android in-app subscription events to Adjust with Qonversion Qonversion sends subscription data for iOS and Android apps to your Adjust account to help you understand your marketing performance. Measure what drives your revenue by tracking trial-to-paying-user conversion, subscription renewals, billing retry state, cancellations, refunds, and other useful subscription events. [Here](integrations-overview) you can find the full list of the events tracked. ## 1. Setup the SDKs 1. Make sure you have Adjust SDK installed. More about the Adjust SDK read [here](https://help.adjust.com/en/developer). 2. Set Qonversion SDKs following [Installing the SDKs](install-sdk) guides. 3. Provide to Qonversion Adjust Advertising ID through [User Properties](user-properties#open-links-in-new-tab): ```swift Swift theme={null} Qonversion.shared().setUserProperty(.adjustAdID, value: Adjust.adid) ``` ```objectivec Objective-C theme={null} [[Qonversion sharedInstance] setUserProperty:QONUserPropertyKeyAdjustAdID value:[Adjust adid]]; ``` ```java Java theme={null} Qonversion.getSharedInstance().setUserProperty(QUserPropertyKey.AdjustAdId, Adjust.getAdid()); ``` ```kotlin Kotlin theme={null} Qonversion.shared.setUserProperty(QUserPropertyKey.AdjustAdId, Adjust.getAdid()) ``` ```dart Flutter theme={null} Qonversion.getSharedInstance().setUserProperty(QUserPropertyKey.adjustAdId, 'your adjust ad id'); ``` ```typescript React Native theme={null} Qonversion.getSharedInstance().setUserProperty(UserPropertyKey.ADJUST_AD_ID, 'your adjust ad id'); ``` ```csharp Unity theme={null} Qonversion.GetSharedInstance().SetUserProperty(UserPropertyKey.AdjustAdId, "your adjust ad id"); ``` ```typescript Cordova theme={null} Qonversion.getSharedInstance().setUserProperty(Qonversion.UserPropertyKey.ADJUST_AD_ID, 'your adjust ad id'); ``` ### Do not track purchase events on the client-side Qonversion tracks and sends revenue events, so if you track revenue events with Adjust SDK as well, you may double count the revenue in your Adjust account. ## 2. Configure the Adjust Integration ## Provide Adjust App Token 1. Navigate to the integrations page in Qonversion and select [Adjust ](https://dash.qonversion.io/app/integration/create?name=adjust). 2. Create Adjust token following [this guide](https://help.adjust.com/en/article/server-to-server-events#set-up-s2s-security) and provide your Adjust App Token to Qonversion. ## Provide OAuth token (Optional) If you've [turned on S2S authentication](https://help.adjust.com/en/article/server-to-server-s2s-security) via OAuth token, add the OAuth token. ## 3. Configure the event mapping Navigate to your app in Adjust, select **All Settings** → **Events**, and create events. Provide generated unique event tokens to Qonversion. ### Attention You must change default Qonversion event names for Adjust integration to the appropriate Adjust event tokens which have been set in the Adjust dashboard. Only events which are mapped to Adjust event tokens will be received by Adjust. Adjust API doesn't support negative revenue values. All values are sent with a positive sign for this integration, including refunds. To switch off sending revenue value for refunds select Subscription Refunded event from the table of the events and click More Options below. Please note that you can expect to have server-side events sent to Adjust account for users who installed the app with Qonversion SDK integrated. Qonversion can't get the data on users who subscribed earlier and never opened the app with Qonversion SDK installed. ### Done Now Qonversion will start sending in-app purchases and subscriptions data to your Adjust account. ## Event Payload In case you need details about data sent to Adjust, follow the example below: Top-level keys carry the Adjust-required envelope; user-scoped fields (`product_id`, `user_id`, `custom_user_id`, `storefront`) are JSON-encoded into the `partner_parameters` and `partner_params` strings (Adjust accepts both names). Device identifiers are platform-dependent: iOS sends `idfa` + `idfv`, Android sends `gps_adid` + `android_id`. ```json iOS theme={null} { "app_token": "", "event_token": "", "s2s": 1, "created_at_unix": 1640912133, "partner_parameters": "{\"product_id\":\"\",\"user_id\":\"QON_...\",\"custom_user_id\":\"\",\"storefront\":\"USA\"}", "partner_params": "{\"product_id\":\"\",\"user_id\":\"QON_...\",\"custom_user_id\":\"\",\"storefront\":\"USA\"}", "adid": "00000000000000000000000000000000", "idfa": "00000000-0000-0000-0000-000000000000", "idfv": "00000000-0000-0000-0000-000000000000", "currency": "USD", "revenue": 34.511 } ``` ```json Android theme={null} { "app_token": "", "event_token": "", "s2s": 1, "created_at_unix": 1640912133, "partner_parameters": "{\"product_id\":\"\",\"user_id\":\"QON_...\",\"custom_user_id\":\"\",\"storefront\":null}", "partner_params": "{\"product_id\":\"\",\"user_id\":\"QON_...\",\"custom_user_id\":\"\",\"storefront\":null}", "adid": "00000000000000000000000000000000", "gps_adid": "00000000-0000-0000-0000-000000000000", "android_id": "0000000000000000", "currency": "USD", "revenue": 34.511 } ``` *** What’s Next * [Amplitude](amplitude) * [User Identifiers](user-identifiers) # AI Image Generation Source: https://documentation.qonversion.io/docs/ai-image-generation Just describe what you want, choose a style, and let the AI handle the rest. Create beautiful, on-brand visuals for your paywalls and onboarding screens — without leaving the Builder and without opening a design tool. Whether you need a quick background, a polished hero image, or a few variations to A/B test, the AI generator gives you fast, flexible options right inside your workflow. *** Generate high-quality visuals directly inside the Builder using text prompts. **What you can use it for** * Abstract gradients and brand backgrounds * Hero visuals for paywalls * Illustrations for onboarding steps * Simple icons or decorative elements * Quick concept sketches before you commit to a final asset Generated images appear in your **Gallery** and can be added to Canvas, or deleted at any time. *** ## Available Models Choose the model that best fits your workflow. Some are optimized for speed, others for final quality. | **Model Name** | **Speed** | **Quality** | **Best For** | | - | - | - | - | | **Flux Schnell** | ⚡ Fast (\~5s) | Good | Quick iterations, previews, early concept exploration | | **Flux Pro** | 🐢 Slow (\~15s) | Excellent | Final assets, high-detail visuals, A/B test creatives | | **Nano Banana** | ⚡ Fast (\~5s) | Excellent | High-quality images with fast turnaround; great general-purpose model | ### Model recommendations * **Flux Schnell** — perfect for exploration. Use it when you want to try multiple prompts quickly. * **Nano Banana** — best all-around balance of speed and quality. * **Flux Pro** — use when you’re ready to produce final paywall or onboarding visuals. *** ## How to generate your first image 1. Add **Image Component** to Canvas **Assets** → **AI Image Generation**. 2. Describe what you want. * Example: “Abstract pastel gradient with soft light, premium feel.” 3. Choose a model . 4. Click Generate. 5. Add Generated Image to your Screen *** ## Prompt tips (that actually help) Great results don’t require long descriptions. Keep things short and visual: * Mention a style: “minimal”, “premium”, “flat illustration”, “soft gradient”. * Mention a mood: “calm”, “playful”, “energetic”. * Mention colors: “lavender + teal”, “black + neon purple”. * For paywalls: “clean background, no text”. * For onboarding: “centered composition”. **Examples you can paste:** * “Minimal abstract gradient, violet and deep blue, subtle glow, soft shapes.” * “Cute playful illustration of a smartphone, bright pastel palette.” * “Premium wave background, glassy texture, purple tones.” *** ## Daily quota To keep performance stable, there’s a simple project-wide limit: * 10 generations **per project** per day * Resets at 00:00 UTC Your remaining quota is shown next to the Generator. When it reaches zero, the UI clearly indicates that you’re out, and it reactivates automatically after the reset. *** [Awards](awards) [Properties Reference](properties-reference) # Localization Source: https://documentation.qonversion.io/docs/ai-localization Translate all text in your No-Code screens into multiple languages, manually or with AI. You can localize all text used in your No-Code screens directly in the Builder. Localization supports manual translation, AI-assisted translation, variables, and live preview, allowing you to create consistent experiences across all languages your app supports. Once localized, **screens automatically display the correct language** based on the user’s **device locale**. If a translation is missing, the **default language** is used as a **fallback.** **Beta Notice**: AI Localization is currently in beta. While it provides fast, high-quality translations for many languages, some results may require manual review — especially for right-to-left (RTL) languages or languages with more complex grammatical structures. We recommend checking all AI-generated translations in the Preview Panel before publishing your screen *** ## Opening the Localization Panel Open localization by selecting the **Localization** button in the Builder toolbar. This opens the **Localization Panel**, where you can view all languages, add new ones, translate content, and preview the results in real time. Your default language appears first and cannot be removed. This is the fallback language for any untranslated content. *** ## Adding & Removing Languages To add additional languages: 1. Open the **Localization Panel.** 2. Select **Add Language.** 3. Choose **one or more** languages from the list. Languages appear using both their display name and ISO code, for example: **French (Belgium) — fr-BE**. To remove a language, click the ellipsis menu (⋯). Removing a language deletes all translations associated with that language. *** ## Editing Translations **The Localization Panel** displays a table of every text element used in your screen. * Each row represents a text key, and each column represents a language. * Select any cell to enter a translation. Changes are saved automatically. ### Variables Text fields that include variables such as `{{price}}`, `{{trial_days}}`, or product attributes can be translated normally. A variable picker is available when editing, allowing you to insert supported variables directly into the translation. Variables cannot be edited or renamed, but they can appear anywhere in your translated text. *** ## Using AI Translation **AI Translation** helps translate content quickly without requiring custom prompts. AI Translation becomes available when your project includes at least two languages. You can translate: * A single language * Multiple selected languages * All languages at once If a translation already exists, the system will ask you whether AI should **overwrite it.** During translation, progress indicators appear in the panel. If an error occurs, you can retry the translation. **AI-generated translations can be edited manually at any time**. *** ## Previewing Localized Screens The Preview Panel shows your screen rendered in the currently selected language. **This helps you verify:** * Line breaks * Layout shifts * Text length differences * Variable placement You can switch between languages and pages using dropdown selectors. Variables appear with sample values (e.g., `{{price}}` → \$9.99) so you can check formatting. The preview updates automatically whenever you change your translations. *** ## Fallback Behavior If a translation **is missing**, the screen uses the **default language** for that text. This ensures your screens always render fully, even if some translations are incomplete. *** ## Autosave All translations are saved **automatically** every five seconds. You can exit the localization view at any time, and your changes will persist. *** ## Import & Export You can import and export all localization data as a JSON file. This is useful for: * Bulk editing translations in external tools (spreadsheets, translation management systems) * Backing up localization before major changes * Sharing translations between team members ### Exporting Translations 1. Open the **Localization Panel**. 2. Click the **Import/Export** button in the header. 3. Select **Export**. 4. A JSON file will download with all your translations. The exported file includes: * Screen metadata (ID, name, export date) * All configured languages * All texts grouped by pages ### Importing Translations 1. Open the **Localization Panel**. 2. Click the **Import/Export** button. 3. Select **Import**. 4. Choose a JSON file from your device. **Import behavior:** * **Screen validation**: The file's `screenId` must match the current screen. * **Language handling**: New languages in the file are automatically added. Existing translations are overwritten. * **Default language**: If the file specifies a different default language, it becomes the new default for the screen. Import will fail if the file contains language codes not supported by the system, or if the `screenId` doesn't match the current screen. ### JSON Format The export file uses the following structure: ```json theme={null} { "screenId": "abc123", "screenName": "My Paywall", "exportedAt": "2026-01-30T12:00:00.000Z", "defaultLanguage": "en-US", "languages": ["en-US", "de-DE", "fr-FR"], "pages": [ { "id": "page-1", "name": "Main Page", "texts": [ { "id": "text-1", "en-US": "Subscribe Now", "de-DE": "Jetzt abonnieren", "fr-FR": "S'abonner maintenant" } ] } ] } ``` | Field | Description | | - | - | | `screenId` | Unique identifier of the screen. Must match when importing. | | `screenName` | Display name of the screen (informational only). | | `exportedAt` | ISO 8601 timestamp of the export. | | `defaultLanguage` | Language code for the default/fallback language. | | `languages` | Array of all language codes in the file. | | `pages` | Array of pages, each containing an array of texts. | *** ## Passing custom locale to the SDKs By default, No-Code screens automatically detect the device's system language and display the appropriate localization if available. You can override this behavior by setting a custom locale, which takes priority over the automatic system language detection. The locale should be in standard format (e.g., `"en"`, `"en-US"`, `"de"`, `"de-DE"`). Pass `nil`/`null` to reset to the system default locale. **Setting locale during initialization:** ```swift Swift theme={null} var config = NoCodesConfiguration( projectKey: "projectKey", locale: "de-DE" ) NoCodes.initialize(with: config) ``` ```kotlin Kotlin theme={null} val config = NoCodesConfig.Builder(context, "projectKey") .setLocale("de-DE") .build() NoCodes.initialize(config) ``` ```java Java theme={null} NoCodesConfig config = new NoCodesConfig.Builder(context, "projectKey") .setLocale("de-DE") .build(); NoCodes.initialize(config); ``` ```typescript React Native theme={null} const noCodesConfig = new NoCodesConfigBuilder("projectKey") .setLocale("de-DE") .build(); NoCodes.initialize(noCodesConfig); ``` ```dart Flutter theme={null} final noCodesConfig = NoCodesConfigBuilder("projectKey") .setLocale("de-DE") .build(); NoCodes.initialize(noCodesConfig); ``` ```csharp Unity theme={null} var noCodesConfig = new NoCodesConfigBuilder("projectKey") .SetLocale("de-DE") .Build(); NoCodes.Initialize(noCodesConfig); ``` ```typescript Cordova theme={null} const noCodesConfig = new Qonversion.NoCodesConfigBuilder("projectKey") .setLocale("de-DE") .build(); Qonversion.NoCodes.initialize(noCodesConfig); ``` ```typescript Capacitor theme={null} const noCodesConfig = new NoCodesConfigBuilder("projectKey") .setLocale("de-DE") .build(); NoCodes.initialize(noCodesConfig); ``` **Setting locale after initialization:** ```swift Swift theme={null} NoCodes.shared.setLocale("fr-FR") NoCodes.shared.showScreen(withContextKey: "yourContextKey") // Reset to system default NoCodes.shared.setLocale(nil) ``` ```kotlin Kotlin theme={null} NoCodes.shared.setLocale("fr-FR") NoCodes.shared.showScreen("yourContextKey") // Reset to system default NoCodes.shared.setLocale(null) ``` ```java Java theme={null} NoCodes.getSharedInstance().setLocale("fr-FR"); NoCodes.getSharedInstance().showScreen("yourContextKey"); // Reset to system default NoCodes.getSharedInstance().setLocale(null); ``` ```typescript React Native theme={null} NoCodes.getSharedInstance().setLocale("fr-FR"); NoCodes.getSharedInstance().showScreen("yourContextKey"); // Reset to system default NoCodes.getSharedInstance().setLocale(null); ``` ```dart Flutter theme={null} NoCodes.getSharedInstance().setLocale("fr-FR"); NoCodes.getSharedInstance().showScreen("yourContextKey"); // Reset to system default NoCodes.getSharedInstance().setLocale(null); ``` ```csharp Unity theme={null} NoCodes.GetSharedInstance().SetLocale("fr-FR"); NoCodes.GetSharedInstance().ShowScreen("yourContextKey"); // Reset to system default NoCodes.GetSharedInstance().SetLocale(null); ``` ```typescript Cordova theme={null} Qonversion.NoCodes.getSharedInstance().setLocale("fr-FR"); Qonversion.NoCodes.getSharedInstance().showScreen("yourContextKey"); // Reset to system default Qonversion.NoCodes.getSharedInstance().setLocale(null); ``` ```typescript Capacitor theme={null} NoCodes.getSharedInstance().setLocale("fr-FR"); NoCodes.getSharedInstance().showScreen("yourContextKey"); // Reset to system default NoCodes.getSharedInstance().setLocale(null); ``` ## Best Practices Finalize your default-language copy before adding translations. * Use variables for prices and durations to keep translations accurate as product metadata changes. * Use AI Translation for an initial pass, then refine manually. * Verify every language in the Preview Panel to ensure text fits your layout. * Keep all languages close to 100% completion before publishing screens. *** [Variables](variables-1) [Fallbacks](fallbacks) # Amazon S3 Source: https://documentation.qonversion.io/docs/amazon-s3 Send raw data reports directly to your Amazon S3 bucket Qonversion allows you to upload all the raw revenue data we track to your Amazon S3 bucket. You can find more details about raw data reports [here](scheduled-reports). ## Prepare Amazon S3 credentials To upload raw data reports to your bucket, Qonversion needs Access Key ID and Secret access key for the user with write access to the corresponding bucket. 1. Navigate to the [IAM Console](https://console.aws.amazon.com/iam/), select the *Users* section and click the *Add users* button 2776 2. Select *Access key - Programmatic access* on the first step. 2818 3. There is no need to add any permissions during the user creation; navigate to the final step. 4. Download the .csv file with the user's credentials; you will need them later. 2758 5. Navigate to the recently created user and save the *User ARN* value; you will need it later. 2756 6. [Navigate to your bucket list](https://console.aws.amazon.com/s3/), and create a new bucket or select an existing one. 7. Copy your bucket name. 2860 8. Create a policy to allow the User ARN from the 5th step to get *PutObject* action access to the bucket from the 7th step. * For example, you can use [AWS Policy Generator](https://awspolicygen.s3.amazonaws.com/policygen.html), where *Principal = User ARN* and *Amazon Resource Name (ARN) = arn:aws:s3:::\[your bucket name from the 7th step]/\** * Or copy the JSON below after replacing *qonversion-test-bucket* and *arn:aws:iam::078705355735:user/QonversionFileUploader* in the *Resource* and *AWS* field, respectively, with your values. ```json theme={null} { "Id": "Policy1672076420793", "Version": "2012-10-17", "Statement": [ { "Sid": "Stmt1672070877286", "Action": [ "s3:PutObject" ], "Effect": "Allow", "Resource": "arn:aws:s3:::qonversion-test-bucket/*", "Principal": { "AWS": [ "arn:aws:iam::078705355735:user/QonversionFileUploader" ] } } ] } ``` 9. Final step to prepare Amazon S3 credentials. Navigate to the *Permissions* section in your bucket, click the *Edit* button in the *Bucket policy* field and paste the JSON from the previous step. Congratulations, after saving the changes, your configuration is ready to use! 2752 ## Enter Amazon S3 credentials to Qonversion 1. Navigate to the [Amazon S3](https://dash.qonversion.io/app/integration/create?name=amazon_s3) integration creation page. 2176 2. Enter *AWS Access Key ID*, *AWS Secret Access Key* and *Bucket name* to the corresponding fields * You have gotten the *AWS Access Key ID* and *AWS Secret Access Key* values with the .csv file, downloaded during the 4th step in the *Prepare Amazon S3 credentials* section above. * You have gotten the bucket's name during the 7th step in the *Prepare Amazon S3 credentials* section above. 3. Click *the Add new integration +* button 4. Congratulations! Now you have successfully connected the Amazon S3 bucket to Qonversion. Navigate to the [Scheduled reports section](scheduled-reports#amazon-s3) to launch report uploading to the bucket. *** What’s Next * [Scheduled Reports](scheduled-reports) # Amplitude Source: https://documentation.qonversion.io/docs/amplitude Send in-app subscription and purchases events to Amplitude Qonversion sends all revenue events, including purchases in your app, subscriptions after free trials, renewals, and even refunds, to your Amplitude account. This allows you to match your user behavior with their payment history in Amplitude and inform your product decisions. ## 1. Setup the SDKs 1. Make sure you have Amplitude SDK installed. More about the Amplitude SDKs read [here](https://help.amplitude.com/hc/en-us/articles/205406607-SDKs). 2. Set Qonversion SDKs following [installing the SDKs](install-sdk) guides. 3. Attribute events sent from Qonversion and events received from the Amplitude SDK to the same user by setting the same user ID to Amplitude that you set to Qonversion SDK using the [setProperty](user-properties) method. ```swift Swift theme={null} Amplitude.instance()?.setUserId("yourSideUserID") ``` ```objectivec Objective-C theme={null} [[Amplitude] instance] setUserId:@"yourSideUserID"]; ``` ```java Java theme={null} Amplitude.getInstance().setUserId("yourSideUserID"); ``` ```kotlin Kotlin theme={null} Amplitude.getInstance().userId = "yourSideUserID" ``` ```csharp Unity theme={null} Amplitude.Instance.init("AMPLITUDE_API_KEY", "yourSideUserID"); ``` If your events from Qonversion do not attribute to users in Amplitude, please check that you are setting user IDs correctly with the SDK. User IDs must be strings with a length of 5 characters or more. **→[Read more about user identifiers here](user-identifiers)** ## 2. Configure the Amplitude Integration 1. Navigate to your Amplitude's Manage Data Section, choose your app, and copy your API Key: 2. Navigate to the Integrations section in your Qonversion project, select [Amplitude](https://dash.qonversion.io/app/integration/amplitude), and provide the API Key. 3. If your Amplitude account is hosted on EU servers, switch your region settings to Europe in the Qonversion Amplitude integration settings. 4. You can use the default event names provided by Qonversion or change them as you need. **→[Read more about tracked events here](integrations-overview#tracked-events)** ### Done! Now Qonversion will start sending in-app purchases and subscriptions data to your Amplitude account. ## Event Payload In case you need details about data sent to Amplitude, follow the example below: Qonversion posts to Amplitude's `/2/httpapi` endpoint, which expects `{api_key, events: [...]}`. Each event includes the keys below. The advertising-id key is platform-dependent: iOS events include `idfa`, Android events include `adid`, and Stripe events omit both. ```json theme={null} { "api_key": "", "events": [ { "user_id": "yourSideUserID", "time": 1590735145100, "productId": "native.subs.full.v3.month.14.99.trial.7d", "event_type": "trial_converted", "insert_id": "5942", "platform": "iOS", "revenueType": "trial_converted", "quantity": 1, "storefront": "USA", "app_version": "11.11", "os_name": "iOS", "os_version": "16.0", "device_model": "iPhone15", "country": "UK", "device_id": "A77BE955-4CFC-A4F5-69D081217E9D3B", "idfa": "00000000-0000-0000-0000-000000000000", "revenue": 2.39 } ] } ``` Amplitude only stores revenue in USD - non-USD currencies are converted server-side. *** [Amazon S3](amazon-s3) [AppMetrica](appmetrica) # Analyse experiments Source: https://documentation.qonversion.io/docs/analyse-experiment Monitor your experiment results with real-time subscription analytics The Analyse section in Qonversion Experiments provides the most critical experiment-related data: * Control vs. Test Variant performance comparison * Number of users exposed to each variant * Number of days the experiment is running Despite the ease of getting all the relevant experiment metrics in real-time, we highly recommend you remember *the Peeking problem* (checking the results and taking action before the A/B test is over). You should avoid accepting or rejecting your hypothesis before the results are statistically significant. ## Results Overview The Overview tab is the first thing you see in the Experiments Analyse tab. Use it to monitor an experiment's progress quickly. ### Experiment result The Experiment result section helps you understand if your results are statistically significant. Suppose the p-value exceeds 0.05 (statistical significance is not reached). In that case, we help you know the optimal sample size per variant to minimize the risk of false negatives and the impacts of temporal effects. ## Primary metric chart Tracking your control vs. test variants' performance during the experiment is essential. Using this chart, you can see the results of your primary metric and select different metrics for the analyses. Please note if you see the lines constantly intersecting, that might mean that the results are not statistically significant and you couldn't accept your hypothesis. ## Variants overview Though each experiment aims to improve only one specific metric, paying attention to other related metrics is essential. Set metrics up in your Variants overview table, and keep an eye on the most important ones. Please note that data and time filtering options from the [Primary metric chart](analyse-experiment#primary-metric-chart) apply to this table and the chart itself. ## Metrics you can analyze with Qonversion Experiments ### Users Calculates as the number of users assigned to the specific variant. ### User-to-Trial Conversion Calculates as the number of *Trial Started* events divided by the number of users assigned to the specific variant. → Numerator: *Trial Started* events from the cohort of users assigned to the variant for the selected period. → Denominator: Number of users assigned to the variant for the selected period. ### User-to-Paid Conversion Calculates as the number of revenue (*Trial Converted, Subscription Started & In-App Purchase*) events divided by the number of users assigned to the specific variant. → Numerator: *Trial Converted, Subscription Started & In-App Purchase* events from the cohort of users assigned to the variant for the selected period. → Denominator: Number of users assigned to the variant for the selected period. ### New Trials Calculates as the number of *Trial Started* events from the cohort of users assigned to the variant for the selected period. ### Trial-to-Paid Conversion Calculates as the number of *Trial Converted* events divided by the number of *Trial Started* events from the cohort of users assigned to the specific variant. → Numerator: *Trial Converted* events attributed to *Trial Started* events. → Denominator: *Trial Started* events from the cohort of users assigned to the specific variant. ### Trials Cancellation Rate Calculates as the number of *Trial Canceled* events divided by the number of *Trial Started* events from the cohort of users assigned to the specific variant. → Numerator: *Trial Canceled* events attributed to *Trial Started* events. → Denominator: *Trial Started* events from the cohort of users assigned to the specific variant. ### New Subscriptions Calculates as the number of *Trial Converted* and *Subscription Started* events from the cohort of users assigned to the variant for the selected period. This includes both direct paid subscription starts and trial-to-paid conversions. ### Subscriptions Cancellation Rate Calculates as the number of *Subscription Canceled* events divided by the number of *Subscription Started* events from the cohort of users assigned to the specific variant. → Numerator: *Subscription Canceled* events attributed to *SubscriptionStarted* events. → Denominator: *Subscription Started* events from the cohort of users assigned to the specific variant. ### Sales Calculates as revenue (after deducting refunds) from the cohort of users assigned to the variant for the selected period. ### Proceeds Calculates as [Sales](analyse-experiment#sales) after deducting App Stores' commission from the cohort of users assigned to the variant for the selected period. ### Refunds Calculates as refund sum from the cohort of users assigned to the variant for the selected period. *** What’s Next * [Pause experiments](pause-experiments) * [Stop experiments](finish-experiment) # Analytics Source: https://documentation.qonversion.io/docs/analytics Access in-app subscription analytics in Qonversion dashboards with customizable filters, grouping, and currency conversion to track revenue and subscriber metrics. ## Overview Note that all data is stored and represented in UTC time zone. To gain comprehensive insights into specific Dashboards, we recommend looking into the following guides for detailed information: * [Monitoring](monitoring) * [New User Overview](users#new-users-overview) * [New-User-to-Trial Conversion](users#new-user-to-trial-conversion) * [New-User-to-Paid Conversion](users#new-user-to-paid-conversion) * [Active Trials](trials#active-trials) * [New Trials](trials#new-trials) * [Trials Movement](trials#trials-movement) * [Trial-to-Paid Conversion](trials#trial-to-paid-conversion) * [Trial Cancellation Rate](trials#trial-cancellation-rate) * [Active Subscriptions](subscriptions#active-subscriptions) * [New Subscriptions](subscriptions#new-subscriptions) * [Subscriptions Movement](subscriptions#subscriptions-movement) * [Subscriptions Cancellation Rate](subscriptions#subscriptions-cancellation-rate) * [Sales](revenue#sales) * [Proceeds](revenue#proceeds) * [ARPU](revenue#arpu) * [ARPPU](revenue#arppu) * [Refunds](revenue#refunds) * [Refund Rate](revenue#refund-rate) * [MRR](revenue#mrr) * [MRR Movement](revenue#mrr-movement) * [ARR](revenue#arr) * [ARR Movement](revenue#arr-movement) * [Events](events-analytics) * [Cohorts](cohorts) * [Customers](customers) * [Apple Search Ads](apple-search-ads-analytics) * [Screen Analytics](no-codes-analytics) *** ## Filters and Groups Filters enable you to narrow down charts to display data that aligns with specific attributes. This feature is handy when examining the performance of particular properties, such as a specific country, currency or app version. On the other hand, groups allow you to dissect the overall totals in Analytics charts into distinct data segments. This functionality proves valuable when comparing the performance of specific properties. ### Available attributes | Attribute | Description | | - | - | | Revenue event | This filter organises data based on revenue events: **Trial Converted, Subscription Started, Subscription Renewed, Subscription Refunded, In-App Purchase** | | Country | This filter organises data based on the recorded device locale | | Product | This filter organises data based on the Product ID in the App Stores | | Currency | This filter organises data based on the currency in which the purchase was made | | Locale | This filter organises data based on the Device language | | Device | This filter organises data based on the Device type | | Device ID | This filter organises data based on the Device ID (identifierForVendor or Settings Secure Android ID) | | OS version | This filter organises data based on the OS version | | App version | This filter organises data based on the Application version | | SDK version | This filter organises data based on the SDK version | | Custom User ID | This filter organises data based on the Custom User ID, which is set with [setProperty](user-properties) method | | Qonversion User ID | This filter organises data based on the User ID generated by Qonversion | | Media Source | This filter organises data based on the Attribution Media Source (Apple Search Ads) | | Campaign | This filter organises data based on the ASA Campaign | | Ad set | This filter organises data based on the set of ASA advertising offers | | Ad | This filter organises data based on the ASA advertising offer | *** ## Date Range Choose the date range or custom amount of days up to the current day you want to analyse. *** ## Time Scale Select the time scale for the x-axis of the charts. Opt for a day timescale to observe data at the finest level of detail. At the same time, lower resolutions like month allow you to identify broader trends over a longer period. *** ## Chart View You can choose the chart view that best suits your data analysis needs, whether you prefer line charts, bar charts or any other type of chart. *** ## Export CSV To export chart data, navigate to the desired chart in the Qonversion dashboard, locate the "Export CSV" button below the chart, and click on it. ## Currency Conversion Qonversion uses currency conversion on the date of purchase. All rates come from [fixer.io](https://fixer.io) - Open-Source API for current and historical foreign exchange rates published by the European Central Bank. ## Display Currency Charts that contain monetary data display values in USD by default. You can choose a different currency to view the data in. Values are converted using exchange rates matching the date of each data point. Your currency preference is saved and persists across sessions. *** [Apple Ads Integration](apple-search-ads) [Monitoring](monitoring) # Analytics Mode Source: https://documentation.qonversion.io/docs/analytics-mode Analytics (Observer) mode gives you best-in-class subscription analytics while you keep your existing in-app purchase flow. Analytics (Observer) mode gives you best-in-class subscription analytics while you keep your existing in-app purchase flow. You can also use [real-time subscriptions events](integrations-overview#tracked-events) in third-party integrations, webhooks, and get accurate Apple Search Ads attribution using this mode. After you install the SDK, follow the steps below. ## 1. Launch SDK Initialize the SDK in Analytics mode: ```swift Swift theme={null} import Qonversion func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let config = Qonversion.Configuration(projectKey: "projectKey", launchMode: .analytics) Qonversion.initWithConfig(config) return true } ``` ```objectivec Objective-C theme={null} #import "Qonversion.h" - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { QONConfiguration *configuration = [[QONConfiguration alloc] initWithProjectKey:@"projectKey" launchMode:QONLaunchModeAnalytics]; [Qonversion initWithConfig:configuration]; return YES; } ``` ```java Java theme={null} import com.qonversion.android.sdk.Qonversion; import com.qonversion.android.sdk.QonversionConfig; import com.qonversion.android.sdk.dto.QLaunchMode; public class App extends Application { @Override public void onCreate() { super.onCreate(); final QonversionConfig qonversionConfig = new QonversionConfig.Builder( this, "projectKey", QLaunchMode.Analytics ).build(); Qonversion.initialize(qonversionConfig); } } ``` ```kotlin Kotlin theme={null} import com.qonversion.android.sdk.Qonversion import com.qonversion.android.sdk.QonversionConfig import com.qonversion.android.sdk.dto.QLaunchMode class App : Application() { override fun onCreate() { super.onCreate() val qonversionConfig = QonversionConfig.Builder( this, "projectKey", QLaunchMode.Analytics ).build() Qonversion.initialize(qonversionConfig) } } ``` ```dart Flutter theme={null} import 'package:qonversion_flutter/qonversion_flutter.dart'; final config = new QonversionConfigBuilder( 'projectKey', QLaunchMode.analytics ).build(); Qonversion.initialize(config); ``` ```typescript React Native theme={null} import Qonversion, { QonversionConfigBuilder, LaunchMode, } from '@qonversion/react-native-sdk'; const config = new QonversionConfigBuilder( 'projectKey', LaunchMode.ANALYTICS ).build(); Qonversion.initialize(config); ``` ```csharp Unity theme={null} using QonversionUnity; private void Start() { QonversionConfig config = new QonversionConfigBuilder( "projectKey", LaunchMode.Analytics ).Build(); Qonversion.Initialize(config); } ``` ```typescript Cordova theme={null} const config = new Qonversion.ConfigBuilder( 'projectKey', Qonversion.LaunchMode.ANALYTICS, ).build(); Qonversion.initialize(config); ``` ```typescript Capacitor theme={null} import Qonversion, { QonversionConfigBuilder, LaunchMode, } from '@qonversion/capacitor-plugin'; const config = new QonversionConfigBuilder( 'projectKey', LaunchMode.ANALYTICS ).build(); Qonversion.initialize(config); ``` **→[Get your Qonversion Project Key](quickstart#2-create-a-project-and-register-your-app)** ## 2. Sync Purchases ### iOS, StoreKit 2 #### Subscriptions only In case you're using StoreKit Version 1, we automatically handle all the needed data about transactions that occurred. However, in the case of StoreKit Version 2, it is necessary to leverage the`syncStoreKit2Purchases()` SDK method. Call this function every time you receive a successful purchase result or auto-renewed transaction. ```swift Swift theme={null} // In case you're using CocoaPods: QonversionSwift.shared.syncStoreKit2Purchases() // In case you're using Swift Package Manager: import QonversionSwift QonversionSwift.shared.syncStoreKit2Purchases() ``` ```objectivec Objective-C theme={null} [[Qonversion sharedInstance] syncStoreKit2Purchases]; ``` ```dart Flutter theme={null} Qonversion.getSharedInstance().syncStoreKit2Purchases(); ``` ```typescript React Native theme={null} Qonversion.getSharedInstance().syncStoreKit2Purchases(); ``` ```csharp Unity theme={null} Qonversion.GetSharedInstance().SyncStoreKit2Purchases(); ``` ```typescript Cordova theme={null} Qonversion.getSharedInstance().syncStoreKit2Purchases(); ``` ```typescript Capacitor theme={null} Qonversion.getSharedInstance().syncStoreKit2Purchases(); ``` #### Subscriptions & Consumables In case you're using StoreKit Version 2 and selling Subscriptions and Consumables we recommend using **one** of these methods **before finishing transactions**: ```swift Swift theme={null} await QonversionSwift.shared.syncStoreKit2Transactions() await QonversionSwift.shared.handleTransaction(transaction) await QonversionSwift.shared.handleTransactions(transactions) ``` ### Android 1. Check whether you have set your products up correctly. Make your GooglePlay subscriptions backwards compatible to have the best possible accuracy in Qonversion. [Learn more here](android-in-app-products) 2. While you are using Qonversion SDKs in Analytics Mode, in-app purchases implementation is entirely on your side. Remember to consume and acknowledge purchases to attribute them to users. **Otherwise, the purchases will be automatically refunded in 3 days.** See the official Android Developer documentation for [processing purchase details](https://developer.android.com/google/play/billing/integrate#process). 3. Sync data with Qonversion in your current purchase flow. Call `syncPurchases()` after every purchase. ```java Java theme={null} Qonversion.getSharedInstance().syncPurchases(); ``` ```kotlin Kotlin theme={null} Qonversion.shared.syncPurchases() ``` ```dart Flutter theme={null} Qonversion.getSharedInstance().syncPurchases(); ``` ```typescript React Native theme={null} Qonversion.getSharedInstance().syncPurchases(); ``` ```csharp Unity theme={null} Qonversion.GetSharedInstance().SyncPurchases(); ``` ```typescript Cordova theme={null} Qonversion.getSharedInstance().syncPurchases(); ``` ```typescript Capacitor theme={null} Qonversion.getSharedInstance().syncPurchases(); ``` ## 3. (Optional) Enable Server-to-Server notifications Qonversion checks user receipts regularly and does not require server-to-server notifications from Apple or Google. Nevertheless, due to these notifications, your analytics charts, third-party integrations and webhooks will work much closer to real-time. * Guide on [Apple Server-to-Server Notifications](ios-s2s-notifications) * Guide on [Google Developer Notifications](google-developer-notifications) ## 4. Set additional user attributes Optionally, to improve attribution in Adjust, AppsFlyer, Singular, or to match Qonversion revenue events to users in [third-party tools](integrations-overview), you can share with us such attributes as User Identifier, IDFA (Identifier for Advertisers, iOS 14.5+ only) or ASID (App set ID, Android 12+ only). Please, follow [this guide](user-properties#custom-user-properties) to learn more. *** ## Next steps With Analytics Mode running, put your subscription data to work. For near real-time charts, also enable [Apple Server-to-Server Notifications](ios-s2s-notifications) and [Google Developer Notifications](google-developer-notifications). # How to read Refund Keeper analytics Source: https://documentation.qonversion.io/docs/analytics-overview What the Refund Keeper section of the Qonversion dashboard shows: the won-back widgets, the activity chart, the refund table with its statuses and answer deadline, and how filtering by Store separates App Store refunds from Google Play chargeback disputes. The **Refund Keeper** section shows what Refund Keeper answered and what it won back, for both stores it supports. For general subscription analytics, see [Subscriptions](/docs/subscriptions) and [Events](/docs/events-analytics). Everything in this section counts only events that Refund Keeper was enabled for. Events from before you enabled a store, and events from a store you never enabled, do not appear here. One more case produces no row at all: if the disputed order cannot be matched to a purchase Qonversion tracks, Refund Keeper still answers the store, but there is nothing to attribute an analytics row to, so the dispute is absent from the widgets and the table. The answer itself is not lost — only its analytics line. ## How do you look at one store only? Filters sit behind the **+** control above the widgets, and **Store** is one of the attributes you can filter on, next to Country, Product, Currency and the rest. A filter applies to the whole section: the widgets, the chart and the table follow it. The table also carries its own **Store** column, so a mixed list stays readable without filtering at all. The two stores count different things: | Store | What one row means | | - | - | | App Store | A refund request Apple asked Qonversion to answer | | Google Play | A chargeback dispute Google asked the developer to review. Regular Google Play refunds are decided by Google without developer input and are never listed here | ## What do the widgets show? Refund Keeper widgets showing requested refunds, won back refunds, the won back rate, pending refunds and expired Of the five tiles described here, four show an amount in your selected currency and **Won Back Rate** shows a percentage; none of them shows a count. For how many items are behind an amount, use the table below, which lists one row per item and prints the total next to its pager. ### Requested Refunds * What the refund requests and chargeback disputes Refund Keeper answered in the period were worth. ### Won Back Refunds * The revenue kept, out of the amount above. * On the App Store this follows Apple's own refund notification. On Google Play it can also be inferred from the order showing no refund seven days after the answer, because Google does not report the verdict. ### Won Back Rate * The share of money the store did not refund, out of everything that is no longer waiting. * Calculated by value, not by count: `Won Back / (Won Back + Refunded + Expired) × 100`. * Events still inside the store's response window are `Pending` and are **excluded from both sides** of the formula, so waiting never drags the rate down. Everything else counts, `Expired` included. * The denominator reaches zero in two different situations. A period with no Refund Keeper events at all has no data to rate. A period that does have events, all of them still `Pending`, has nothing decided yet — **Pending Refunds** tells you how much is waiting. In neither case does the period have a rate, so read whatever the widget shows there as "no result yet" rather than as a bad result. ### Pending Refunds and Expired * **Pending Refunds** is the money still inside the store's response window — exactly what the rate above leaves out of both of its sides. * **Expired** is the money that left the window with no outcome recorded yet. On Google Play this is not a final state; see [the statuses below](#what-do-the-statuses-mean). The widget row is configurable, so the set you see may differ from the one described here. ## Refund activity timeline A timeline of answered events and won-back results across the selected period, following any filter you apply. The refund activity timeline with daily requested and won back series ## What is in the refund table? The Refund Keeper requests table with UID, transaction, product, status, store, deadline, price and date columns | Column | Meaning | | - | - | | **UID** | Qonversion user ID. Opens the customer profile with this item highlighted | | **Transaction id** | The store transaction identifier | | **Product** | The product that was purchased | | **Status** | `Pending`, `Refunded`, `Saved` or `Expired`, described below | | **Store** | App Store or Google Play, so a list covering both stores stays readable without a filter | | **Deadline** | What is left of the store's answer window, counted from the request. It counts down while the item is `Pending` (`5h 11m left`), switches to `Answered` once the item has an outcome, and to `Expired` if the window closed without one. `Answered` here is about the outcome landing — Refund Keeper's own answer went out much earlier, which is why a `Pending` row still shows a countdown | | **Price** | The disputed amount in your selected currency | | **Date Created** | When the store asked for the answer | ### What do the statuses mean? | Status | Meaning | | - | - | | `Pending` | Answered or queued, and the store has not revealed a decision yet | | `Refunded` | The store decided in the customer's favour | | `Saved` | The payment was kept. On the App Store this follows Apple's own refund notification; on Google Play it can be inferred from the order showing no refund seven days after the answer, since Google never reports the verdict | | `Expired` | The response window passed and no outcome has been recorded **yet**. Every row in this table is an event Refund Keeper already answered, so `Expired` never means "we failed to answer". On Google Play it is not final either: the row becomes `Saved` once the seven-day check confirms no refund | The table has a tab per status plus **All**, and you can search it by UID or transaction ID. ### How long is the answer window? | Store | Window counted in the Deadline column | | - | - | | App Store | 12 hours from the request | | Google Play | 24 hours from the notification | ### Why is a Google Play outcome late? Google does not return the verdict when Refund Keeper answers a dispute. Qonversion detects the result afterwards from Google's follow-up notifications and the order state, so a Google Play row sits without an outcome after the answer was already sent, then moves to `Saved` or `Refunded`. If seven days pass with no refund on the order, the dispute is recorded as won back, and the recorded outcome time is that seven-day mark. The row itself flips about two days later than that: refund rows can still materialise with a lag, so the check deliberately waits before concluding that no refund is coming. An App Store outcome needs no such inference — it arrives with Apple's own refund notification. ## What to keep in mind 1. Refund Keeper analytics covers only the period when Refund Keeper was enabled for that store. 2. It covers **App Store refund requests** and **Google Play chargeback disputes**. Regular Play Store refunds are out of scope. 3. The App Store requires [App Store Server Notifications V2](ios-s2s-notifications); Google Play requires [Real-time developer notifications](google-developer-notifications) pointed at Qonversion. 4. A `Saved` row means the store kept the payment this time. It is not a guarantee for future events, because the store decides each case on its own. *** [Enable Refund Keeper](enable-refund-keeper) [Raw Data Export](raw-data-reports) # Android 4.+ migration guide Source: https://documentation.qonversion.io/docs/android-4-migration-guide Historical migration guide for upgrading the Qonversion Android SDK to major version 4.+: instance-based initialization, renamed entities, and updated purchase and entitlement methods. ## Upgrading version Increase the dependency version in your app *build.gradle* file to upgrade your Qonversion SDK to the latest version ```groovy build.gradle theme={null} dependencies { implementation 'io.qonversion.android.sdk:sdk:4.+' } ``` ## Initialization Qonversion Android SDK 4 contains significant changes in how the library is initialized. We are moving from a singletons approach to the instance-based one. Before, you initialized Qonversion using the `launch` call: ```kotlin Kotlin theme={null} class App : Application() { override fun onCreate() { super.onCreate() Qonversion.launch(this, "projectKey", false) } } ``` ```java Java theme={null} public class App extends Application { @Override public void onCreate() { super.onCreate(); Qonversion.launch(this, "projectKey", false); } } ``` Now, instead, you should create a `QonversionConfig` object using nested `Builder` and provide it to initialization method as follows: ```kotlin Kotlin theme={null} class App : Application() { override fun onCreate() { super.onCreate() val qonversionConfig = QonversionConfig.Builder( this, "projectKey", QLaunchMode.SubscriptionManagement ).build() Qonversion.initialize(qonversionConfig) } } ``` ```java Java theme={null} public class App extends Application { @Override public void onCreate() { super.onCreate(); final QonversionConfig qonversionConfig = new QonversionConfig.Builder( this, "projectKey", QLaunchMode.Infrastructure ).build(); Qonversion.initialize(qonversionConfig); } } ``` Note that instead of providing the `observeMode` flag to the `launch` call, you should provide a concrete value from the `QLaunchMode` enum, depending on which mode you use Qonversion. Also, we've renamed our modes to make them more transparent for users: * "Observe" mode becomes "Analytics" mode, * "Infrastructure" mode becomes "Subscription Management" mode. After the initialization, you can access the Qonversion instance whenever you want as follows: ```kotlin Kotlin theme={null} Qonversion.shared ``` ```java Java theme={null} Qonversion.getSharedInstance(); ``` So you should replace all your Qonversion calls with the construction above. Also, if you were using `Qonversion.setDebugMode()` for testing purposes, you should now call the `setEnvironment(QEnvironment.Sandbox)` method of the `QonversionConfig.Builder`. As no `launch` method is available anymore, you won't get `QLaunchResult` as a result. The good news is that there are analogues for all the fields you might have been using from there: * for `uid`, call `userInfo()` and get the `QUser.qonversionId` from the result, * for `products` call `products()`, * for `offerings` call `offerings()`, * for `permissions` call `checkEntitlements()`, * `experiments` field has no analogue, as the `experiments` method was removed for now. ## Entitlements We are on the way to renaming permissions to entitlements as this naming suits more what it is used for. So, the following objects and methods were renamed in this release: | Version `<4` | Version 4+ | | - | - | | QPermission | QEntitlement | | QonversionPermissionsCallback | QonversionEntitlementsCallback | | QProductRenewState | QEntitlementRenewState | | QPermissionSource | QEntitlementSource | | QPermissionsCacheLifetime | QEntitlementsCacheLifetime | | checkPermissions | checkEntitlements | | UpdatedPurchasesListener | QEntitlementsUpdateListener | The `QEntitlement` class contains the same information as the `QPermission` with small renamings. * `permissionsID` was renamed to `id`, * `productID` was renamed to `productId`, * `isActive` became a Kotlin property, so it should now be accessed as a property. There is no `setUpdatedPurchasesListener` method in Qonversion. You should provide `UpdatedEntitlementsListener` to `QonversionConfig.Builder` during the initialization using the `setUpdatedEntitlementsListener` method. The same change is made to the `setPermissionsCacheLifetime` method. Now you can set the required lifetime using `QonversionConfig.Builder.setEntitlementsCacheLifetime` while Qonversion initialization. ```kotlin Kotlin theme={null} val qonversionConfig = QonversionConfig.Builder( this, "projectKey", QLaunchMode.SubscriptionManagement ) .setEntitlementsUpdateListener(object : EntitlementsUpdateListener { override fun onEntitlementsUpdated(entitlements: Map) { // handle updated entitlements } }) .setEntitlementsCacheLifetime(QEntitlementsCacheLifetime.Year) .build() Qonversion.initialize(qonversionConfig) ``` ```java Java theme={null} final QonversionConfig qonversionConfig = new QonversionConfig.Builder( this, "projectKey", QLaunchMode.Infrastructure ) .setEntitlementsCacheLifetime(QEntitlementsCacheLifetime.Year) .setEntitlementsUpdateListener(entitlements -> {/* handle updated entitlements */}) .build(); Qonversion.initialize(qonversionConfig); ``` ## Automation changes We've also changed the way the `Automations` is used. As in Qonversion, you should use `Automations` via the shared instance. On the first access, it will be initialized and returned. Then the initialized instance will be used. ### You should access the shared instance of `Automations` strictly after you initialize Qonversion, else an exception will be thrown. ```kotlin Kotlin theme={null} public class App : Application { override fun onCreate() { super.onCreate() val qonversionConfig = QonversionConfig.Builder( this, "projectKey", QLaunchMode.SubscriptionManagement ).build() Qonversion.initialize(qonversionConfig) Automations.shared.setDelegate(...) } } ``` ```java Java theme={null} public class App extends Application { @Override public void onCreate() { super.onCreate(); final QonversionConfig qonversionConfig = new QonversionConfig.Builder( this, "projectKey", QLaunchMode.Infrastructure ).build(); Qonversion.initialize(qonversionConfig); Automations.getSharedInstance().setDelegate(...); } } ``` Also, the methods for working with push notifications were moved from `Qonversion` to `Automations`, so if you were using the following methods: * `setNotificationsToken`, * `handleNotification`, * `getNotificationCustomPayload`, make sure to make calls from the Automations instance instead of the Qonversion one. ## Rest of the changes Along with the changes described above, there are several technical improvements and other changes in the new major release: * all the internal classes and extensions are now marked as `internal`, so they won't be accessible from your code and won't pollute your project namespace (note that they are still accessible in java because of interoperability issues, but you should prevent using them as well). All these classes and extensions were moved to the `internal` package to make the library file structure more readable; * the deprecated methods `resetUser`, `setUserId`, and `handleNotification(RemoteMessage)` were removed. `resetUser` was deprecated for a long time, and it did nothing, so there is nothing to replace this call with, remove the method if you were still using it for some reason. `setUserId` should be replaced with the `setProperty` call with `QUserProperties.CustomUserId` parameter. Instead of using the `handleNotification` method, accepting `RemoteMessage`, call the method of the same name, accepting the data map (`RemoteMessage.data`). Also note that the `handleNotification(Map)` method was moved to the `Automations` class, as described above; * the `firebase-messaging` dependency causing resolution collisions was removed; * the `experiments` method was removed - we are now working on a new design of A/B experiments; * `QEntitlementsCacheLifetime` enum values are rewritten in CamelCase to match the rest code style; * the `AttributionSource` enum was renamed to `QAttributionProvider` and moved to the `dto` package; * the `UserProperties` enum was renamed to the `QUserProperty` enum and moved to the `dto` package; * the `checkTrialIntroEligibilityForProductIds` method was shortened to `checkTrialIntroEligibility`; * added the new method `userInfo`, which returns the information about the current Qonversion user. Now it contains internal Qonversion and identity identifiers. The user info may be extended in future releases; * added new enum values - `QOfferingTag.Unknown` and `QTrialDuration.Unknown`, which are used when parsing fails. `QTrialDuration.Unknown` is now a default value for the `QProduct.trialDuration` field, which is no longer nullable. *** [iOS 3.+ migration guide](ios-3-migration-guide) [Flutter 5.+ migration guide](flutter-5-migration-guide) # Android 6.+ migration guide Source: https://documentation.qonversion.io/docs/android-6-migration-guide Historical migration guide for upgrading the Qonversion Android SDK to major version 6.+: renamed public methods, entities, and packages, with a before/after reference table. ## Upgrading version Increase the dependency version in your app *build.gradle* file to upgrade your Qonversion SDK to the latest version ```groovy build.gradle theme={null} dependencies { implementation 'io.qonversion.android.sdk:sdk:6.+' } ``` ## Renamings In this major version, we have renamed several entities, public methods and changed some packages to make our namings and structure more clear. To make everything work just change all the occurrences from the left column of the table below to the new ones. | Before upgrade | After upgrade | | - | - | | `QUserProperty` | `QUserPropertyKey` | | `setUserProperty` | `setCustomUserProperty` | | `setProperty` | `setUserProperty` | | `com.qonversion.android.sdk.dto.QUserProperty` | `com.qonversion.android.sdk.dto.properties.QUserPropertyKey` | | `com.qonversion.android.sdk.dto.QEntitlement` | `com.qonversion.android.sdk.dto.entitlements.QEntitlement` | | `com.qonversion.android.sdk.dto.QEntitlementRenewState` | `com.qonversion.android.sdk.dto.entitlements.QEntitlementRenewState` | | `com.qonversion.android.sdk.dto.QEntitlementSource` | `com.qonversion.android.sdk.dto.entitlements.QEntitlementSource` | | `com.qonversion.android.sdk.dto.QEntitlementsCacheLifetime` | `com.qonversion.android.sdk.dto.entitlements.QEntitlementsCacheLifetime` | ## User properties changes As mentioned above, we have renamed the `QUserProperty` enum to `QUserPropertyKey`. It was made to release the name `QUserProperty` for a class, containing both the property key and its value. This class will be used in the new Qonversion method `userProperties`, which will return all the properties set for the current user. ```java Java theme={null} Qonversion.getSharedInstance().userProperties(new QonversionUserPropertiesCallback() { @Override public void onSuccess(@NonNull QUserProperties userProperties) { for (QUserProperty property : userProperties.getProperties()) { Log.d("User property", "Key: " + property.getKey() + ", value: " + property.getValue()); } } @Override public void onError(@NonNull QonversionError error) { // handle error here } }); ``` ```kotlin Kotlin theme={null} Qonversion.shared.userProperties(object : QonversionUserPropertiesCallback { override fun onSuccess(userProperties: QUserProperties) { userProperties.properties.forEach { Log.d("User property", "key: ${it.key}, value: ${it.value}") } } override fun onError(error: QonversionError) { // handle error here } }) ``` `QUserProperties` class returned as the successful result of `userProperties` contains several useful fields and methods to get all the types of properties you may want: | Field | Description | | - | - | | `properties` | List of all user properties | | `definedProperties` | Subset of all user properties, which were set using Qonversion-defined keys | | `customProperties` | Subset of all user properties, which were set using custom keys | | `flatPropertiesMap` | A flattened version of all user properties as a key-value map | | `flatDefinedPropertiesMap` | A flattened version of defined user properties as a key-value map | | `flatCustomPropertiesMap` | A flattened version of custom user properties as a key-value map | | Method | Arguments | Description | | - | - | - | | `getProperty` | `key` - string | Searches for a property with the given property `key` in all properties list | | `getDefinedProperty` | `key` - `QUserPropertyKey` | Searches for a property with the given Qonversion-defined property `key` in the defined properties list. | The last change here is that we've enriched `QUserPropertyKey` enum with several values: * added a new property key `AdvertisingID`, which is used on iOS but can be received on Android due to cross-platform user management; * added a `Custom` key to represent all the custom properties. *** [iOS 5.+ migration guide](ios-5-migration-guide) [Flutter 7.+ migration guide](flutter-7-migration-guide) # Android SDK 6.x to 7.x migration guide Source: https://documentation.qonversion.io/docs/android-7-migration-guide We've upgraded the Google Play Billing Library dependency to version 6 in this release, which has led to major changes in the product and subscription management parts of our SDK. These are described below. ## Upgrading version Increase the dependency version in your app *build.gradle* file to upgrade your Qonversion SDK to the latest version ```groovy build.gradle theme={null} dependencies { implementation 'io.qonversion.android.sdk:sdk:7.+' } ``` ## Product updates ### Qonversion and Google Play product link changes Google Play has recently upgraded the structure of their subscription products. We've written about it [in our blog](https://qonversion.io/blog/google-play-billing-library-5-0/). In short, they combined the same subscriptions with different durations into a single subscription with different base plans. In addition, they announced different kinds of offers to base plans, including free trials or introductory prices. In this release, we begin to support those changes. Previously Qonversion **subscription** products required a Google Play subscription identifier. Now you can also set a specific **base plan** for the subscription. Different base plans of the same subscription can be linked to different Qonversion products. Let's have a look at an example. Let's say you sell monthly and annual subscriptions in your app, both on Android and iOS. With the previous Qonversion SDK versions, you would specify two Qonversion products (`premium_monthly` and `premium_annual`), two App Store products (`premium_monthly_ios` and `premium_annual_ios`), and two Google Play products (`premium_monthly_android` and `premium_annual_android`). Then you would link those store products to Qonversion products as follows: `premium_monthly` - `premium_monthly_ios` and `premium_monthly_android`, `premium_annual` - `premium_annual_ios` and `premium_annual_android`. With the new Google Play Subscriptions structure, you can create only one subscription for Android - `premium_android` and two base plans for it (`monthly` and `annual`). With this done, you can now use a single subscription for both Qonversion products as follows: `premium_monthly` - `premium_monthly_ios` and `premium_android` with `monthly` base plan, `premium_annual` - `premium_annual_ios` and `premium_android` with `annual` base plan. ### Store details for the `QProduct` class Speaking of the SDK side - the first big part of the changes is the addition of the new `basePlanId` field to `QProduct` which can be set to the product via Qonversion Dashboard. We have deprecated the `QProduct.skuDetails` field, because `SkuDetails` contain information only for backward compatible base plans and are now deprecated in the Google Play Billing Library. Instead, we have added the new `storeDetails` field of type `QProductStoreDetails`, which contains all the store information for the Google Play Product. You can find the specifications of that class [on this page](google-play-product-details). If `basePlanId` is not specified for a product, it means one of the following: * it is a one-time (in-app) product that can not have any base plans, * it is a product that was not updated in the Qonversion dashboard after this version was released. In the case of the in-app product, both `skuDetails` and `storeDetails` fields will contain information about that in-app. But in the second case, when there is a subscription product without a specified base plan, the `skuDetails` field will contain information about the **backward compatible** base plan, while the `storeDetails` field will only contain common subscription information without base-plan-specific data. It means that you should start using `storeDetails` field for base-plan-specific data (like price, duration, and so on) for subscription products only if you've specified their base plan IDs in Qonversion product settings. ### The other `QProduct` fields changes Along with the above changes, we have changed the `type` field and removed the `duration` and `trialDuration` fields from the product class. The `type` and `duration` were previously being set via the dashboard, and the `trialDuration` was calculated from the store details and was represented as an enumeration with several common durations. Now, the product `type` is calculated from the store details. We've also added the `Intro` value to `QProductType` to separate `trial` and `intro` products. An `Unknown` value was also added for the cases where we are unable to determine the product type. The `duration` and `trialDuration` fields were replaced with `subscriptionPeriod` and `trialPeriod`. Both properties are of type `QSubscriptionPeriod` and the values are also calculated from the store details. The last thing to note is that we've also changed the `prettyPrice` field calculation. Earlier it was calculated from `skuDetails`, while now we first try to use `storeDetails` if possible and only then fall back to `skuDetails`. ### `QProduct` updates summary Below is the shortened summary of the `QProduct` changes: * new `basePlanId` field, specifying concrete subscription base plan, added, * `skuDetails` field deprecated, * new `storeDetails` field, containing information about Google Play product, added; * the `type` field is now calculated based on store details, instead of the value set via the Qonversion dashboard. The `Intro` and `Unknown` values were added to `QProductType` to separate cases of trial and intro products and also the case when we are unable to determine the product type; * the `duration` and `trialDuration` fields were replaced with `subscriptionPeriod` and `trialPeriod` fields of type `QSubscriptionPeriod` with the values calculated from the store details; * the `prettyPrice` field now prefers `storeDetails` to get the price from, and only then - `skuDetails`. ## Purchase flow updates In this release, we have changed the `purchase` and `updatePurchase` methods signatures. Previously there were several methods for different sets of arguments. Now we've arranged all those properties into classes - `QPurchaseModel` and `QPurchaseUpdateModel`, that should be instantiated and provided to the single `purchase` or `updatePurchase` method. You can create instances of those classes either manually or by using the utility functions `toPurchaseModel` and `toPurchaseUpdateModel` added to the `QProduct` class. Below is an example of how the `purchase` flow has changed: ```kotlin Kotlin theme={null} // Old Qonversion.shared.purchase(this, product, callback = object: QonversionEntitlementsCallback {...}) // New val purchaseModel = product.toPurchaseModel() Qonversion.shared.purchase(this, purchaseModel, callback = object: QonversionEntitlementsCallback {...}) ``` ```java Java theme={null} // Old Qonversion.getSharedInstance().purchase(this, product, new QonversionEntitlementsCallback() {...}); // New final QPurchaseModel purchaseModel = product.toPurchaseModel(); Qonversion.getSharedInstance().purchase(this, purchaseModel, new QonversionEntitlementsCallback() {...}); ``` And an `updatePurchase` flow example: ```kotlin Kotlin theme={null} // Old Qonversion.shared.updatePurchase(this, product, "oldProductId", callback = object: QonversionEntitlementsCallback {...}) // New val purchaseUpdateModel = product.toPurchaseUpdateModel("oldProductId") Qonversion.shared.updatePurchase(this, purchaseUpdateModel, callback = object: QonversionEntitlementsCallback {...}) ``` ```java Java theme={null} // Old Qonversion.getSharedInstance().updatePurchase(this, newProduct, "oldProductId", new QonversionEntitlementsCallback() {...}); // New final QPurchaseUpdateModel purchaseUpdateModel = newProduct.toPurchaseUpdateModel("oldProductId"); Qonversion.getSharedInstance().updatePurchase(this, purchaseUpdateModel, new QonversionEntitlementsCallback() {...}); ``` If you were using only product identifiers, you can create purchase models manually as follows: ```kotlin Kotlin theme={null} // Old Qonversion.shared.purchase(this, "productId", callback = object: QonversionEntitlementsCallback {...}) // New val purchaseModel = QPurchaseModel("productId") Qonversion.shared.purchase(this, purchaseModel, callback = object: QonversionEntitlementsCallback {...}) ``` ```java Java theme={null} // Old Qonversion.getSharedInstance().purchase(this, "productId", new QonversionEntitlementsCallback() {...}); // New final QPurchaseModel purchaseModel = new QPurchaseModel("productId"); Qonversion.getSharedInstance().purchase(this, purchaseModel, new QonversionEntitlementsCallback() {...}); ``` ```kotlin Kotlin theme={null} // Old Qonversion.shared.updatePurchase(this, "newProductId", "oldProductId", callback = object: QonversionEntitlementsCallback {...}) // New val purchaseUpdateModel = QPurchaseUpdateModel("newProductId", "oldProductId") Qonversion.shared.updatePurchase(this, purchaseUpdateModel, callback = object: QonversionEntitlementsCallback {...}) ``` ```java Java theme={null} // Old Qonversion.getSharedInstance().updatePurchase(this, "newProductId", "oldProductId", new QonversionEntitlementsCallback() {...}); // New final QPurchaseUpdateModel purchaseUpdateModel = new QPurchaseUpdateModel("newProductId", "oldProductId"); Qonversion.getSharedInstance().updatePurchase(this, purchaseUpdateModel, new QonversionEntitlementsCallback() {...}); ``` If necessary, you can provide a specific offer identifier for subscription purchase. ```kotlin Kotlin theme={null} // Specifying offer via the `toPurchaseModel` method: val productOfferDetails = ... // Choose an offer from `storeDetails` val purchaseModel = product.toPurchaseModel(productOfferDetails) // Specifying offer ID via the `toPurchaseModel` method: val purchaseModel = product.toPurchaseModel("offer_id") // Specifying offer ID via the constructor: val purchaseModel = QPurchaseModel("productId", "offer_id") val purchaseUpdateModel = QPurchaseUpdateModel("newProductId", "oldProductId", "offer_id") // Specifying offer ID after the purchase model creation: purchaseModel.offerId = "offer_id" ``` ```java Java theme={null} // Specifying offer via the `toPurchaseModel` method: final QProductOfferDetails productOfferDetails = ...; // Choose an offer from `storeDetails` final QPurchaseModel purchaseModel = product.toPurchaseModel(productOfferDetails); // Specifying offer ID via the `toPurchaseModel` method: final QPurchaseModel purchaseModel = product.toPurchaseModel("offer_id"); // Specifying offer ID via the constructor: final QPurchaseModel purchaseModel = new QPurchaseModel("productId", "offer_id"); final QPurchaseUpdateModel purchaseUpdateModel = new QPurchaseUpdateModel("newProductId", "oldProductId", "offer_id"); // Specifying offer ID after the purchase model creation: purchaseModel.setOfferId("offer_id"); ``` If provided, we will try to find and purchase the offer with the specified ID for the requested Qonversion product. If there is no offer with the specified ID, an error will be returned. If no offer ID is provided for the subscription purchase of Qonversion product with a specified base plan ID, then we will choose the most profitable offer for the client from all the available offers. We calculate the cheapest price for the client by comparing all the trial or intro phases and the base plan. For old Qonversion products (where the base plan ID is not specified), as well as for in-app products, the offer ID is ignored. You can also remove any intro/trial offer from the purchase (use only a bare base plan). For that purpose, you should call `removeOffer` method of purchase model: ```kotlin Kotlin theme={null} purchaseModel.removeOffer() ``` ```java Java theme={null} purchaseModel.removeOffer(); ``` ## The other changes * Google Play Billing Library version was upgraded from 6.0.1 to the latest 6.1.0; * The minimal supported SDK version was upgraded from 16 to 19, the Kotlin version was also upgraded from 1.6 to 1.8; * The `checkTrialIntroEligibility` method was improved and now detects the eligibility based on store details; * `QonversionErrorCode.SkuDetailsError` was removed; * The `QProductDuration` and `QTrialDuration` classes were removed; * The previously deprecated method `contextForScreenIntent` was removed from `AutomationsDelegate`. *** [\[Jan 2024\] Migration guide. Google Play Billing Library 6.](qonversion-sdk-major-version-copy) [Flutter SDK 7.x to 8.x migration guide](flutter-8-migration-guide) # Android SDK 7.x to 8.x migration guide Source: https://documentation.qonversion.io/docs/android-8-migration-guide We've upgraded the Google Play Billing Library dependency to version 7 in this release, leading to several changes in our SDK. These are described below. ## Upgrading version Increase the dependency version in your app *build.gradle* file to upgrade your Qonversion SDK to the latest version ```groovy build.gradle theme={null} dependencies { implementation 'io.qonversion.android.sdk:sdk:8.+' } ``` ## Deployment upgrades With the new Google Play Billing Library 7 we've increased our `minSdkVersion` to 21 and `targetSdkVersion` to 34. If you were using lower `minSdkVersion` you should upgrade it to 21 to use the latest version of our SDK and Google Play Billing Library. If you have already targeted the 21+ version, you should do nothing. ## Installment plans support In the latest library version, Google introduces installment plans, when a customer commits to pay for several subsequent subscription periods. In our release, we've added a new field `installmentPlanDetails` to `QProductOfferDetails` with the details of the installment plan if they exist. It contains the following information: | Field | Type | Description | | - | - | - | | `commitmentPaymentsCount` | Int | Committed payments count after a user signs up for this subscription plan | | `subsequentCommitmentPaymentsCount` | Int | Subsequent committed payments count after this subscription plan renews | We've also added an `isInstallment` flag to `QProductStoreDetails` to check if the current product has an installment plan or not. Below is an example of those fields usage: ```java Java theme={null} Qonversion.getSharedInstance().products(new QonversionProductsCallback() { @Override public void onSuccess(@NonNull Map products) { QProduct installmentProduct = products.get("installmentProductId"); if (installmentProduct == null) { // Product not found return; } QProductStoreDetails storeDetails = installmentProduct.getStoreDetails(); if (storeDetails != null && storeDetails.isInstallment()) { QProductOfferDetails basePlan = storeDetails.getBasePlanSubscriptionOfferDetails(); if (basePlan == null) { // No base plans exist return; } QProductInstallmentPlanDetails installmentPlanDetails = basePlan.getInstallmentPlanDetails(); // Use the installment plan information } } @Override public void onError(@NonNull QonversionError error) { // Handle error here } }); ``` ```kotlin Kotlin theme={null} Qonversion.shared.products(object : QonversionProductsCallback { override fun onSuccess(products: Map) { val installmentProduct = products["installmentProductId"] val storeDetails = installmentProduct?.storeDetails if (storeDetails?.isInstallment == true) { val installmentPlanDetails = storeDetails.basePlanSubscriptionOfferDetails?.installmentPlanDetails // Use the installment plan information } } override fun onError(error: QonversionError) { // Handle error here } }) ``` ## Fallback files We're happy to introduce the fallback files support in this release to make our system reliability even higher. Fallback files allow your app to work as expected in rare cases of network connection or Qonversion API issues for new users without a cache available. This allows purchases and entitlements to be processed for new users even if the Qonversion API faces issues. This also makes it possible to receive remote configs for cases when the network connection is unavailable. Read more about the fallback files in [the documentation](system-reliability#fallback-files). ## Error codes changes In this release, we have renamed several `QonversionErrorCode` values to make them more clear. | Old name | New name | | - | - | | `UnknownError` | `Unknown` | | `CanceledPurchase` | `PurchaseCanceled` | | `ProductUnavailable` | `StoreProductNotAvailable` | | `ParseResponseFailed` | `ResponseParsingFailed` | If you were using any of the mentioned codes, please, update the names with the right column values. ## The other changes * Pending purchases support was added for prepaid subscriptions. * `QIntroEligibilityStatus` `NonIntroProduct` was renamed to `NonIntroOrTrialProduct` to better reflect the meaning. *** [\[Jul 2024\] Migration guide. Google Play Billing Library 7.](jan-2024-migration-guide-google-play-billing-library-6-copy) [Flutter SDK 8.x to 9.x migration guide](flutter-9-migration-guide) # Android In-App Products Setup Source: https://documentation.qonversion.io/docs/android-in-app-products Create and configure Android in-app products and subscriptions in Google Play Console, including base plans, offers, pricing, and activation. Creating in-app products before [granting users access](android-store-setup#6-grant-the-service-account-access-in-google-play-console) or [enabling the required Google Cloud APIs](android-store-setup#2-enable-the-required-google-apis) for the service account may cause problems with the receipt validation using Google Play Developer API. Google Play Developer API can return the following error *"The current user has insufficient permissions to perform the requested operation."* When encountering this issue, open the In-app products or Subscription tab respectively on the Google Play Console and make any updates. For example, edit the product description and save it. In order to create an in-app purchase navigate to [Google Play Developer Console](https://play.google.com/apps/publish/) and select the 'All apps' tab. Then select your app from the list. Select **Products**. You can select either In-app products or Subscriptions. **1. In-app products** Let's choose the In-app products tab. Click the **Create product** button. You have to provide Product ID, Name, and Description. ### Note You can’t change the product ID after the product has been created. Set the price at the bottom of the page and apply changes. ### Note Qonversion doesn't yet support multi-quantity purchases, so leave that option unselected. Finally, check the tax and compliance settings and then click **Save**. Once you have created a product, it has an **Inactive** status. Click the **Activate** button. **2. Subscriptions** Let's choose the Subscriptions tab. Click the **Create subscription** button. Provide Product ID and Name. Now the subscription is created but it is not configured yet. There are four steps to configure it. Two of them are optional. The first step is optional. You can add up to four custom strings explaining what users get when they subscribe. For the next two steps, you should create base plans and offers, if necessary. The base plan contains basic information about the subscription such as duration, price, renewal type, grace period, etc. You can check our [blog post](https://qonversion.io/blog/google-play-billing-library-5-0/) for the details of the new subscription model of Google Play. To create a base plan click **Add a base plan** either from the task list or from the base plans and offers section. Enter the base plan identifier and configure its renewal type with billing and grace periods. You may also add tags that are used to distinguish base plans from the API side. This is not required with one base plan per subscription. The last step is to set the price. Navigate to "Prices and availability" section and click **Set prices**, select regions the subscription will be available in and press **Set price**. Enter the price and click **Update**. Save changes. Once you have created a base plan, it has a **draft** status. Click the **Activate** button to make it available to users. Your base plan is ready to use. You can use the created subscription. You can also add options like a trial period or discounts to your subscriptions. This is where offers come in. Offers belong to base plans. Click **Add offer** to create an offer. Select the base plan to which the new offer will belong and click **Add offer**. Specify offer identifier and select eligibility criteria. There are several options available: users who never bought this or any other subscriptions; those, who upgraded from other subscriptions; developer-determined. You may also add tags as for the base plan. The final step is phases. You can configure up to two phases which will be used before the base plan purchasing. For example, you can add a free trial for a week and a 10% discount for the next week before the user will buy the original subscription. To create a phase click the **Add phase** button in the Phases section. Choose the phase type, duration, and, if you chose discount type, prices. In the example below we create a 10% discount price for a week. Press **Apply** and **Save** to create an offer. The offer has a **draft** status. Press **Activate**. After you have done all the above steps your subscription is ready to use. *** [Google Play Store](google-play) [Android test devices](android-test-devices) # Android Source: https://documentation.qonversion.io/docs/android-sdk Install Qonversion Android SDK to implement in-app subscriptions, validate user receipts, get subscription analytics, and third-party integrations. [![GitHub release](https://img.shields.io/github/v/release/qonversion/android-sdk?label=Latest%20Release)](https://github.com/qonversion/android-sdk/releases) ## Install the library The recommended way to install the Qonversion library for Android is with a build system like Gradle. The library is distributed via [Maven Central](https://search.maven.org). Add the Qonversion library to the dependencies section in your app **build.gradle** ```groovy build.gradle theme={null} dependencies { implementation 'io.qonversion.android.sdk:sdk:9.+' } ``` Or check the current release version on [Github](https://github.com/qonversion/android-sdk/releases) if you want to use a specific version. ## Kids Mode for Qonversion Android SDK In case you are building an App for Kids, [follow this guide](kids-mode-sdk) to learn about Kids Mode for Qonversion SDKs. *** ## Next steps The SDK is installed. Choose the mode to implement — this determines how much code you write next. # Set up Google Play for Qonversion Source: https://documentation.qonversion.io/docs/android-store-setup How to create a Google Play service account, grant Qonversion access to the Google Play Developer API, and connect Real-Time Developer Notifications — the prerequisites for processing Android purchases through Qonversion. Qonversion needs two things from your Google Play account: a **service account** with permission to read your app's billing data, and a Pub/Sub topic for **Real-Time Developer Notifications (RTDN)**. This guide walks through adding the service account key, connecting Google to create the topic, and entering that topic in Google Play Console. The flow spans two consoles — **Google Cloud Console** (sections 1–5) and **Google Play Console** (section 6) — then finishes in **Qonversion Dash** and **Google Play Console** (section 7). Keep both Google tabs open; you'll switch between them. ## 1. Open the Google Cloud Console 1. Sign in to the [Google Cloud Console](https://console.cloud.google.com/welcome). 2. Pick the Google Cloud project that owns your app from the project picker at the top (or create one). 3. From **Quick access** on the welcome page, open **APIs & Services** — this is where you enable the integrations Qonversion needs. Use a project that's dedicated to this app (or shared across your apps). The service account you create later inherits this project's identity, so keep things tidy. ## 2. Enable the required Google APIs Enable these two APIs in the Google Cloud project that contains your service account: * **Google Play Android Developer API** — lets Qonversion validate purchases and read subscription state. * **Cloud Pub/Sub API** — lets Qonversion create the topic and subscription used for Real-Time Developer Notifications (RTDN). The [Google Play Developer Reporting API](https://developers.google.com/play/developer/reporting/reference/rest) is not required for this connection. Qonversion determines RTDN status from notifications it receives. ### 2.1 Open the API Library In **APIs & Services**, click **Library** in the left sidebar. ### 2.2 Find each API Search by name (e.g. `Google Play Android Developer API`) and open the matching result. Repeat for the other API. ### 2.3 Enable each API On every Product details page, click **Enable**. You'll repeat this for both APIs. ## 3. Create a service account The service account is the identity Qonversion uses to call Google APIs on your behalf. ### 3.1 Open IAM & Admin Return to the Cloud Console home (or use the search bar) and open **IAM & Admin** from **Quick access**. ### 3.2 Start the service account form In the left sidebar, click **Service Accounts**, then **+ Create service account** at the top. ### 3.3 Name the service account Give it a clear name like `Qonversion`. The Service account ID auto-fills from the name. Click **Create and continue** to move on. ### 3.4 Grant the required roles In the **Permissions** step, grant **Pub/Sub Admin** to the service account in this Cloud project, then click **Done**. Qonversion uses it to create the RTDN topic and subscription and grant Google Play permission to publish to the topic. Leave the optional **Principals** step empty. If you monitor Pub/Sub in Google Cloud, grant [Cloud Monitoring](https://docs.cloud.google.com/pubsub/docs/monitoring) access separately. ## 4. Generate a JSON key The JSON key is the credential Qonversion stores to authenticate as your service account. ### 4.1 Open the Keys tab and add a key Open the service account you just created, switch to the **Keys** tab, click **Add key**, then **Create new key**. ### 4.2 Choose JSON and create Pick **JSON** (the recommended format) and click **Create**. The browser downloads the key file — save it somewhere safe. The key file is downloadable only at creation time. If it is already saved in Qonversion and working, losing your local copy does not require replacement. To replace it, keep the old key active, create a new key for the same service account, and upload the new JSON file in Qonversion. Click **Save & verify** and confirm the new key shows **Verified** before deleting the old key. Contact Qonversion support if verification does not succeed. ## 5. Copy the service account's email You'll need this email in a moment to invite the service account into Google Play Console. In the **Service Accounts** list, click the email address (or its copy icon) — it looks like `@.iam.gserviceaccount.com`. ## 6. Grant the service account access in Google Play Console The service account exists in Google Cloud, but Google Play Console manages permissions separately — you need to invite it there too. ### 6.1 Open Users and permissions In [Google Play Console](https://play.google.com/console), open **Users and permissions** from the left sidebar and click **Invite new users**. ### 6.2 Paste the email and add your app under App permissions Paste the service account email you copied in section 5 into **Email address**, then open the **App permissions** tab and add the app you're configuring. ### 6.3 Switch to Account permissions and grant access Open the **Account permissions** tab, tick these three checkboxes, then click **Invite user**: 1. **View app information and download bulk reports (read-only)** 2. **View financial data, orders, and cancellation survey responses** 3. **Manage orders and subscriptions** It can take up to 24 hours for Google's API to recognize new service-account permissions. If Qonversion shows "Please provide valid JSON credentials" right after pasting the key, give it a few hours and retry. ## 7. Paste everything into Qonversion Now jump back to **Qonversion Dash → Project Settings → Stores → Google Play Console**. 1. Paste your **Android Package Name**. This is the same value as `applicationId` in your app-level `build.gradle` — and the identifier Google Play Console lists against each app on the **Home** screen of your developer account. 2. Open **Service account key** and upload the `.json` file you downloaded in section 4. 3. Click **Save & verify**. The saved key shows **Verified** when Google accepts it. **Full access** means the key can also read your products for product sync; **Limited access** means it cannot. 4. Under **Real-time notifications (RTDN)**, check for a Pub/Sub topic. If none is shown, open [Google Play store setup](https://dash.qonversion.io/no-codes/stores/android) for the same project and click **Connect to Google**. Return to **Real-time notifications (RTDN)** and copy the topic. 5. Paste the topic in Google Play Console → **Monetization setup → Real-time developer notifications**. See [Google Developer Notifications](google-developer-notifications) for the Play Console step. ## Troubleshooting ### "Please provide valid JSON credentials" * Wait up to 24 hours after granting Play Console access — propagation is slow. * Make sure the service account is invited in **both** Google Cloud (as a service account) **and** Google Play Console (as a user with the three required Account permissions plus the app under App permissions). * Confirm the JSON file's `client_email` matches the service-account email you invited. ### "The current user has insufficient permissions" If you created your in-app products before granting the service account access, Google Play sometimes caches the old permission set. Open any in-app product on the Google Play Console, edit it lightly (e.g. tweak the description) and save — that nudges the cache. ### Real-time notifications show "Not receiving" * Use **Send Test Notification** in Google Play Console to [test notification delivery](https://developer.android.com/google/play/billing/getting-ready#configure-rtdn), or trigger a sandbox subscription event. * Confirm that the topic name in Google Play Console matches the topic shown in Qonversion. * Check that `google-play-developer-notifications@system.gserviceaccount.com` has **Pub/Sub Publisher** on that topic. Qonversion grants this permission when it creates the topic. * Qonversion creates the topic with your service account; confirm it has **Pub/Sub Admin** in its Google Cloud project. * Check that **Cloud Pub/Sub API** is enabled in the same Google Cloud project. ### Lost the .json key If the current key is already saved in Qonversion and working, no action is needed. Otherwise, create a new key in **Service Accounts → Keys**, then follow the [replacement steps above](#4-generate-a-json-key) before deleting the old key. *** ## What's next? After this step, your Android configuration is complete. Test end-to-end: * Make a sandbox purchase from a test device and confirm it appears in Qonversion within seconds. * Trigger a refund or cancellation in Play Console and confirm the event arrives. # Android test devices Source: https://documentation.qonversion.io/docs/android-test-devices Set up Android test devices, emulators, and license test accounts to safely test Google Play in-app purchases and subscriptions. **1. Test devices** You can use both physical devices and emulators to run your app and test in-app purchases. Make sure that the emulator has the Play Store installed. You can check that by selecting **Tools->AVD Manager** in Android Studio. 303 Emulators must use an emulator image with Google Play Store. **2. Test account** The test account from the [License testing](license-testing) step must be added to the test device **first**. **3. Add a PIN if needed** When you make an in-app purchase for the first time, Google may ask you to verify your account. The following message may appear: "Something went wrong", even if you enter the correct password. 382 To get rid of this error on your device navigate to **Settings->Accounts->** choose your account from the list. Select Google Account: Info, security & personalization. You have to finish signing in to continue. Click **Sign in**. 372 Google advises to add a lock screen. Click the **Next** button. 378 You can choose any convenient screen lock. For instance, let's choose **Continue without fingerprint**. Select and set **PIN** for security. 369 You may need to click on **Sign in** again to update the data. Finally the authorization process is over and you can make in-app purchases. 368 *** [Android In-App Products Setup](android-in-app-products) [Creating an App in Google Play Console](app-creation-in-google-play-console) # Creating an App in Google Play Console Source: https://documentation.qonversion.io/docs/app-creation-in-google-play-console Walk through creating and configuring a new app in the Google Play Console before connecting it to Qonversion. ## 1. Create an app Navigate to the [Google Play Console](https://play.google.com/console) and select the **All apps** tab from the menu on the left, then click **Create app**. Fill in the details required and click on **Create app**. ## 2. Set up the app You have to complete the steps below to set up your app. **2.1 App access** If your application has any forms of authentication, you have to provide the access details. **2.2 Content ratings** Complete the questionnaire to receive your content ratings for the app. **2.3 Target audience and content** Manage [target audience and app content](https://support.google.com/googleplay/android-developer/answer/9867159) settings. **2.4 News app** Answer the question about the use of ads in your application. **2.5 App category** Select your app category and provide your contact details. **2.6 Set up the store listing** Fill in the app's name and description. Provide the app icon and screenshots that will be shown on Google Play. **2.7 Merchant account** If you don't have a merchant account yet, [create a payments profile](https://support.google.com/googleplay/android-developer/answer/7161426?hl=en). **2.8 Set the price for your app** If you choose to offer your app for free, you cannot change that in the future. *** What’s Next * [App publication in Google Play Console](app-publication-in-google-play-console) * [License testing](license-testing) * [Android test devices](android-test-devices) * [Android In-App Products Setup](android-in-app-products) # Publish your Android App Source: https://documentation.qonversion.io/docs/app-publication-in-google-play-console If you want to test in-app purchases, your app must be published (either production, alpha or beta channels, ). **1. Create closed track** Navigate to [Google Play Developer Console](https://play.google.com/apps/publish/) and select the 'All apps' tab. Then choose your app from the list. Select the **Testing->Closed Testing** tab in the left menu bar. Select Alpha from the Active tracks list and click **Manage track** opposite it. 1853 Click on the **Create new release** button. **2. Upload the signed .apk file** Upload the [signed](https://developer.android.com/studio/publish/app-signing#sign-apk) App Bundle or APK file. 1562 After uploading the file, save the release. 1574 **3. Add countries/regions for test** You can target your Beta track release to users in [specific countries](https://support.google.com/googleplay/android-developer/answer/7550024?hl=en). Add available countries to test the track. 1169 **4. Configure a list of testers (Optional)** If you want your release to be available to certain users on Google Play, but not visible to anyone else, please specify a [list of testers](https://play.google.com/console/about/closed-testing/). Click the **Create email list** button. 1084 Add email address and click **Save changes**. 949 After the application is published, testers will be able to join your test. 722 **5. Roll out** The release is currently in Draft status. Click the **Edit** button. 438 Click the **Review release** button to check the release. 399 If the release is ready, click the **Start rollout to Alpha** button. 371 ### Before making a purchase make sure your release is approved and available to selected testers. 352 ### Publishing time When you publish your application for the first time, it may take up to several days. Later releases usually take several hours. ### Make sure that the uploaded App Bundle or APK file matches the one you are testing when it comes to version code, version name, applicationId and keystore signature. *** What’s Next * [License testing](license-testing) * [Android In-App Purchases Setup](android-in-app-products) * [Android test devices](android-test-devices) # App-Specific Shared Secret Source: https://documentation.qonversion.io/docs/app-specific-shared-secret App-Specific Shared Secret is a key to receive receipts for auto-renewable in-app subscriptions. See how to generate the app's shared secret below. You need to provide the app-specific shared secret to Qonversion, so we can verify your in-app purchases. Qonversion provides out-of-the-box in-app subscription infrastructure. Learn more about [Qonversion](https://qonversion.io) and [sign up](https://dash.qonversion.io/site/signup?utm_source=docs\&utm_medium=docs\&utm_campaign=sharedsecret) for free. Follow the steps below to get the shared secret for your iOS app: ## 1. Navigate to the In-App Purchases section in App Store Connect 1. [Log in](https://appstoreconnect.apple.com/login) to your App Store Connect account. 2. Choose [Apps](https://appstoreconnect.apple.com/apps) and select the App. 3. Select the App Information section from the left side menu. 4. Scroll down to the "App-Specific Shared Secret" label and click the "Manage" button. ## 2. Generate the shared secret Generate and copy your shared secret. *** What’s Next Learn more about App Store Connect Shared Secret in our blog. * [App Store Connect Shared Secret](https://qonversion.io/blog/app-store-connect-shared-secret/) # App Store Connect API Key Source: https://documentation.qonversion.io/docs/app-store-connect-api-key How to add an App Store Connect API key for product sync, app settings, and verified revenue reports. The **App Store Connect API key** lets Qonversion read your app's metadata directly from App Store Connect. We use it to: * **Sync your in-app purchase catalog and prices** using your app's App Store ID. * **Read your subscription grace period configuration**. With report access and your **Vendor number**, the key also lets Qonversion read **sales reports** for verified revenue reporting. Without this key, Qonversion cannot sync your App Store catalog and prices or read its sales reports. This key is separate from the [In-App Purchase API key](in-app-purchase-api-key), which is used for App Store Server API access and promotional offer signing. The In-App Purchase key cannot sync your catalog or sales reports, and the App Store Connect key cannot sign promotional offers. [Apple Ads](apple-search-ads) has a separate connection: **Connect with Apple** for new connections and an Apple Ads API key for projects already connected with one. Neither App Store key on this page is an Apple Ads credential. ## 1. Make sure you have the right role An **Account Holder** or **Admin** can generate a team API key. The role of the person generating the key is separate from the key's **Access** setting. Choose **App Manager** access for product and price sync and subscription grace period settings. If you also use **Verified revenue reports**, choose **Admin** access for the same key and add your **Vendor number** in Qonversion. Contact your team's Account Holder or Admin if you cannot generate a team key. ## 2. Open the App Store Connect API tab 1. [Log in](https://appstoreconnect.apple.com/login) to App Store Connect. 2. Click **Users and Access** in the top navigation. 3. Open the **Integrations** tab. 4. Make sure the **App Store Connect API** sub-tab is selected (not "In-App Purchase"). ## 3. Generate a new key 1. Click **Generate API Key** (or **+** if you already have keys). 2. Give it a clear name, e.g. **Qonversion**. 3. Set **Access** to **App Manager** for catalog and price sync. Choose **Admin** if you also use **Verified revenue reports**. 4. Click **Generate**. ## 4. Copy the Key ID and Issuer ID, and download the .p8 private key Everything you need lives on the same screen — copy the two identifiers and download the key file before leaving: * Copy the **Key ID** (10-character string in the row of your new key) — this is **Key ID** in Qonversion. * Above the table, copy the **Issuer ID** (UUID format, e.g. `57246542-96fe-1a63-e053-0824d011072a`) — this is **Issuer ID** in Qonversion. * In the same row, click **Download API Key** to get the **.p8** file. Apple lets you download the `.p8` file **only once**. Save it somewhere safe before continuing. If this key is already saved in Qonversion, losing your local copy does not require you to replace it. ## 5. Paste everything into Qonversion 1. Open Qonversion Dash → **Project Settings** → **Stores** → **Apple App Store**. 2. Under **Credentials on file**, open **App Store Connect API key** (**Set up** the first time, **Edit** once a key is saved): * Paste the **Key ID** in the **Key ID** field. * Upload the `.p8` file in the **Private Key** field. * Paste the **Issuer ID** in the **Issuer ID** field. 3. Click **Save & verify**. The page shows which capabilities Apple confirmed for the key. Add your **App Store ID** on the **Product sync** row to sync that app's catalog. For **Verified revenue reports**, add your **Vendor number** on that row. ## Troubleshooting ### "Failed to verify credentials" — HTTP 401 / NOT\_AUTHORIZED * Make sure you pasted the **Key ID**, not the key name. * Make sure the **Issuer ID** is from the **App Store Connect API** tab, not the In-App Purchase tab. * Make sure the `.p8` file matches the Key ID you pasted. ### Product sync says the key cannot sync products Create a new team key with **App Manager** access for product sync, or **Admin** if you also use **Verified revenue reports**. Save it with **Edit**, then follow the replacement steps below before revoking the old key. ### Lost the .p8 file Apple does not let you re-download the file. If the key is already saved in Qonversion and still works, no action is needed. To replace it, keep the old key active, generate a new team key, and save its `.p8`, Key ID, and Issuer ID with **Edit → Save & verify**. Confirm the displayed Key ID matches the new key, it shows **Verified**, and **What this key unlocks** confirms the capabilities you use. If you use this key for App Store Server API requests, ask Qonversion support to confirm the replacement before revoking the old key. Revoke the old key after these checks and any needed support confirmation. *** ## What's next? To finish setting up the App Store connection: * **App-specific shared secret** — add your [App-Specific Shared Secret](ios-app-store-info-fields#2-generate-the-app-specific-shared-secret) so Qonversion can validate your app's App Store receipts. * **In-App Purchase key** — add an [In-App Purchase API key](in-app-purchase-api-key) for promotional offers and App Store Server API access. * **Server-to-server notifications** — copy the [Server-to-Server Notification URL](ios-s2s-notifications) into App Store Connect. # App Store Privacy Source: https://documentation.qonversion.io/docs/app-store-privacy Understand what data is being collected by Qonversion for App Store reporting purposes You’ll need to provide information about your app’s privacy practices to submit new and update existing apps in App Store Connect, including the practices of third-party partners whose code you integrate into your app. This information is required starting December 8, 2020. You can learn more [here](https://developer.apple.com/app-store/app-privacy-details/). We have prepared a short guide on data that Qonversion collects to help you provide the required information to the App Store Connect. Below is the list of the data types that Apple requires to report. We have indicated if Qonversion collects any specific type of data. App Store also requires to submit the purpose of data collection. You should select Analytics and App Functionality for the data types collected by Qonversion. | Data Type | Details | | - | - | | Contact Info | Not collected by Qonversion. *You can choose to use Qonversion [User Properties](user-properties) to store user name, email, and other contact information.* | | Health and Fitness | Not collected by Qonversion | | Financial Info | Not collected by Qonversion | | Location | Not collected by Qonversion | | Sensitive Info | Not collected by Qonversion | | Contacts | Not collected by Qonversion | | User Content | Not collected by Qonversion | | Browsing History | Not collected by Qonversion | | Search History | Not collected by Qonversion | | Identifiers | **Qonversion collects User ID and Device ID.** | | Purchases | **Qonversion collects App Store purchases history** | | Usage Data | Not collected by Qonversion | | Diagnostics | Not collected by Qonversion | | Other Data | **Qonversion collects data on device OS, make, model, and resolution.** | *** [App Store Offer Codes](appstore-offer-codes) [App Store Promoted Purchases](app-store-promoted-purchases) # App Store Promoted Purchases Source: https://documentation.qonversion.io/docs/app-store-promoted-purchases Promoting Your In-App Purchases on the App Store Page and Search Results Promoted in-app purchases appear on your app page and can be displayed in search results. App Store promoted purchases are enabled by default. If a user starts an in-app purchase from the App Store, the purchase flow begins when a user opens your app. Kindly note that the scenario described below is related only to the iOS platform. ## iOS ### Set delegate If you need additional logic, set `QONPromoPurchasesDelegate`. ```swift Swift theme={null} Qonversion.shared().setPromoPurchasesDelegate(self) ``` ```objectivec Objective-C theme={null} [[Qonversion sharedInstance] setPromoPurchasesDelegate:self]; ``` ### Add the following function to handle promoted purchases ```swift Swift theme={null} func shouldPurchasePromoProduct(withIdentifier productID: String, executionBlock: @escaping Qonversion.PromoPurchaseCompletionHandler) { // check AppStore productID value in case you want to enable promoted purchase only for specific products let completion: Qonversion.PurchaseCompletionHandler = {result, error, flag in // handle the purchased product or error } // call this block if you want to allow promoted purchase or just store block and call when needed // do nothing and do not call block if you don't want to allow purchase executionBlock(completion) } ``` ```objectivec Objective-C theme={null} - (void)shouldPurchasePromoProductWithIdentifier:(NSString *)productID executionBlock:(QONPromoPurchaseCompletionHandler)executionBlock { // check AppStore productID value in case you want to enable promoted purchase only for specific products QONPurchaseCompletionHandler completion = ^(NSDictionary *result, NSError *_Nullable error, BOOL cancelled) { // handle purchased product or error }; // call this block if you want to allow promo purchase or just store block and call when needed // do nothing and do not call block if you don't want to allow purchase executionBlock(completion); } ``` ## Flutter ### Listen to the promo purchase stream `Qonversion.promoPurchasesStream` emits AppStore `productID` whenever a promoted purchase flow is triggered. Listen to the stream to handle promoted purchases. ```dart Flutter theme={null} StreamSubscription _promoPurchasesStream; _promoPurchasesStream = Qonversion.getSharedInstance().promoPurchasesStream.listen((productID) async { // check AppStore productID value in case you want to enable promoted purchase only for specific products // call `Qonversion.promoPurchase` if you want to allow promoted purchase or just store productID and call when needed // don't call `Qonversion.promoPurchase` if you don't want to allow purchase }); ``` ### Complete the purchase Call `Qonversion.promoPurchase` when you want to initialize the purchase (for example, immediately after the onboarding screens) and pass the AppStore `productID` as the parameter. Alternatively, call this method immediately on the app start. ```dart Flutter theme={null} try { final entitlements = await Qonversion.getSharedInstance().promoPurchase(productID); // handle entitlements here } catch (e) { // handle error here print(e); } ``` ## React Native and Cordova ### Listen to promo purchases events Provide `PromoPurchasesDelegate` to get notified whenever a promoted purchase flow is triggered. ```typescript React Native theme={null} Qonversion.getSharedInstance().setPromoPurchasesDelegate({ onPromoPurchaseReceived: async (productId, promoPurchaseExecutor) => { // check AppStore productId value in case you want to enable promoted purchase only for specific products // call `promoPurchaseExecutor` if you want to allow promoted purchase or just store productId and executor and call it when needed // don't call `promoPurchaseExecutor` if you don't want to allow purchase }, }); ``` ```typescript Cordova theme={null} Qonversion.getSharedInstance().setPromoPurchasesDelegate({ onPromoPurchaseReceived: async (productId, promoPurchaseExecutor) => { // check AppStore productId value in case you want to enable promoted purchase only for specific products // call `promoPurchaseExecutor` if you want to allow promoted purchase or just store productId and executor and call it when needed // don't call `promoPurchaseExecutor` if you don't want to allow purchase }, }); ``` ### Complete the purchase Call the `promoPurchaseExecutor` provided to the delegate right in the delegate method or later when you want to initialize the purchase (for example, immediately after the onboarding screens) and pass the AppStore `productId` as the parameter. ```typescript React Native theme={null} try { const entitlements = await promoPurchaseExecutor(productId); // handle entitlements here } catch (e) { // handle error here } ``` ```typescript Cordova theme={null} try { const entitlements = await promoPurchaseExecutor(productId); // handle entitlements here } catch (e) { // handle error here } ``` ## Unity ### Handle events `Qonversion.PromoPurchasesReceived` sends the event when a user initiates a promotional in-app purchase from the App Store. Declare a delegate to handle promo purchases. Promo purchases will proceed automatically if you are not using the`PromoPurchasesReceived` event. ```csharp Unity theme={null} Qonversion.GetSharedInstance().PromoPurchasesReceived += HandlePromoPurchases; private void HandlePromoPurchases(string appStoreProductId, StartPromoPurchase startPromoPurchase) { // check AppStore productID value in case you want to enable promoted purchase only for specific products // call `startPromoPurchase` if you want to allow promoted purchase or just store productID and call when needed // don't call `startPromoPurchase` if you don't want to allow purchase } ``` ### Complete the purchase Call `startPromoPurchase` when you want to initialize the purchase (for example, immediately after the onboarding screens). ```csharp Unity theme={null} startPromoPurchase((entitlements, error) => { if (error == null) { if (entitlements.TryGetValue("premium", out Entitlement premium) && premium.IsActive) { // Handle the active entitlement here } } else { // Handle the error Debug.Log("Error" + error.ToString()); } }); ``` *** [App Store Privacy](app-store-privacy) [App Store Small Business Program](app-store-small-business-program) # App Store Small Business Program Source: https://documentation.qonversion.io/docs/app-store-small-business-program Set up your App Store Small Business Program details in Qonversion ## 1. Apple Small Business Program Overview * Existing developers who made up to 1 million USD in proceeds in 2020 for all their apps, as well as developers new to the App Store, can qualify for the program and the reduced commission. Navigate to your App Store Connect account to apply. * If a participating developer surpasses the 1 million USD threshold, the standard commission rate will apply for the remainder of the year. * If a developer’s proceeds fall below the 1 million USD threshold in a future calendar year, they can re-qualify for the 15% commission the year after. * Developers must identify any Associated Developer Accounts to determine proceeds eligibility. You can find the details of the program [here](https://qonversion.io/blog/how-to-enroll-in-the-new-app-store-small-business-program/). Enrollment is currently opened for the program. ## 2. Set up Small Business Program details in Qonversion Qonversion can accurately calculate your app's proceeds after excluding Apple's commission taking into account your participation in the Small Business Program. You can choose to send the proceeds to any integration you use. ### Navigate to your project settings dashboard in Qonversion You have to set the dates for your participation in the Small Business Program for each project you have with Qonversion. So if you have a number of apps don't forget to provide the details for each of them. ### Select the dates for your participation in the program Set the end date to the end of the next year if you don't expect to quit the program earlier. If at some point in time you chose to quit the program or you exceed the \$1M threshold don't forget to update the end date in Qonversion. Apple's commission after the end date will be calculated at a standard Apple rate. Please note that if you chose the start date earlier than the current date, Qonversion will not resend the data to integrations to avoid any duplicates. *** [App Store Promoted Purchases](app-store-promoted-purchases) [Apple Family Sharing](apple-family-sharing) # Read the Apple Ads report Source: https://documentation.qonversion.io/docs/apple-ads-report How to open the Apple Ads report in Qonversion and read its Apple and Qonversion metrics, Growing and Mature cohort windows, Gross and Net revenue, spend history, and differences from the Apple Ads console. The Apple Ads report is available to accounts with it enabled — contact support. Once it is enabled and your project is [connected to Apple Ads](apple-search-ads), select **Switch to Apple Ads** (or **Try the new report** in the banner) on **Analytics → Apple Search Ads**; the report then opens from **Apple Ads** in the sidebar. The report has Campaigns, Ad groups, and Keywords levels. A chart opened from a campaign, ad group, or keyword follows that exact entity; keep the same dates and filters when comparing it with the table or an Apple export. ## Two sources, two kinds of installs Apple supplies spend, impressions, taps, and Apple's tap-through installs. Qonversion supplies customers and revenue attributed by the Qonversion SDK and AdServices. These install counts can differ: a Qonversion **User** is an SDK-attributed customer, while an Apple **Install** is a console-reported conversion. **CPA** is spend divided by Apple installs; **CAC** is spend divided by Qonversion paying customers. They answer different questions. Period totals and daily chart points are also different from cohort windows. A period total covers the selected dates. A D7 window asks what customers **installed on those dates** earned through the end of the UTC day seven days after each install date. It does not mean revenue collected during the next seven calendar days. `D0` ends at the end of the install's UTC day; Lifetime continues to change and never closes. ## Growing and Mature cohort windows The **Cohort windows** setting offers two readings: * **Growing (default)** includes all installs in the selected period and their revenue observed so far, capped at each window's UTC boundary. Its cost denominator is the row's Apple spend for the period. Values keep accumulating as customers pay and Apple revises spend. Growing values are displayed plainly; there is no **so far** hatch or maturity legend in this mode. * **Mature** counts closed cohort days only and marks windows that are still maturing as **so far**. A cohort day is closed once its window length plus the maturation buffer has passed and Apple can no longer restate that day's spend. The report names the closed date range. The two modes answer different questions and can differ for a young period. Confirm which mode the report or CSV used before comparing values. A closed window fixes the cohort date range; later refunds or revenue corrections can still change its revenue. **“—” means unavailable, not zero.** A measured zero is displayed as zero. ## Revenue basis and refunds Choose **Gross** or **Net** in the report. **Net** is revenue after the store's commission and, where tax data is available, taxes. Refunds are reported separately. **Revenue** is already net of refunds; don't subtract the **Refunds** column again. Closed windows can still change when late refunds or revenue corrections arrive. ## Report dates and historical spend Report dates and window boundaries use **UTC**, while Apple's console uses the advertising organization's time zone. Today is partial, and a daily comparison can shift across the date boundary. [Apple's daily reporting API](https://developer.apple.com/documentation/apple_ads/ReportingRequest) allows a start date no more than 90 days in the past. To add spend from before the report's history starts, open **Spend history…** in the report's ⋯ menu and upload an Apple Ads custom report (Day, Campaign, Country or Region; Spend, Impressions, Taps, Installs). **Spend history…** is shown only to users who can manage Apple Ads. You review what the file adds before anything is saved. ## Compare with the Apple Ads console Report dates use UTC; the Apple Ads console uses your organization's time zone, so compare whole UTC days. **Installs** are Apple's tap-through installs, while **Users** are customers attributed through the Qonversion SDK, so the two counts differ. **CPA** is spend per Apple install; **CAC** is spend per paying user. Compare the same campaign, dates, country and Gross/Net setting, starting with Spend, Impressions, Taps and Installs. “—” means no value is available, not zero. If a difference remains, contact support with your project ID and date range — never send Apple credentials. # Apple App Store Source: https://documentation.qonversion.io/docs/apple-app-store Overview of Qonversion's Apple App Store guides: offer codes, App Store privacy, promoted purchases, the Small Business Program, Family Sharing, promotional offers, and migrating from SwiftyStoreKit. This section covers the Apple App Store features and setup steps you can use with Qonversion on iOS. Each guide is self-contained; pick the one that matches what you need to configure. Let users on iOS 14+ redeem offer codes through a one-time redemption URL or in-app via the `presentCodeRedemptionSheet` API. Reference of the data types Qonversion collects so you can complete your App Store Connect app privacy details (report as Analytics and App Functionality). Show your in-app purchases on your App Store page and in search results. Promoted purchases are enabled by default and are iOS-only. Qualify for Apple's reduced 15% commission on up to 1 million USD in annual proceeds, and set up your program details in Qonversion. Let a user share a subscription with up to 5 family members. Supported out-of-the-box in Subscription Management Mode once products and entitlements are configured. Re-engage lapsed subscribers or retain current ones with limited-time discounts and free trials on auto-renewable subscriptions (macOS, iOS, tvOS). Move your iOS app from SwiftyStoreKit to Qonversion to add server-side receipt validation, real-time monitoring, and subscription analytics. # Apple Family Sharing Source: https://documentation.qonversion.io/docs/apple-family-sharing Apple Family Sharing allows a user to share a subscription with up to 5 other family members. Family Sharing can help increase customer engagement and improve retention of your app. To begin, you can visit App Store Connect to turn on Family Sharing for a specific product. You can navigate to your apps page and select 'Turn On' in the Family Sharing section. Note that once you turn on Family Sharing for a product, you cannot turn it off. Qonversion [Subscription Management mode](subscription-management-mode) supports Family Sharing out-of-the-box. You just need to set up [your products and entitlements](subscription-management-mode#1-configure-products--entitlements). Family Sharing will be available automatically. ## Family Sharing Implementation 1. After the initial purchase, every family member will receive the transaction in`func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction])` method with `restored` type 2. You will only see the transactions from the first user who made a purchase in the Qonversion dashboards. *** [App Store Small Business Program](app-store-small-business-program) [Migrate from SwiftyStoreKit](swiftystorekit-alternative) # Apple Promotional Offers Source: https://documentation.qonversion.io/docs/apple-promotional-offers Learn how to work with Apple Promotional Offers using Qonversion SDKs ### This section is about [Apple Promotional Offers](https://developer.apple.com/documentation/storekit/in-app_purchase/original_api_for_in-app_purchase/subscriptions_and_offers/implementing_promotional_offers_in_your_app) Please do not confuse them with [Apple Introductory Offers](https://developer.apple.com/documentation/storekit/in-app_purchase/original_api_for_in-app_purchase/subscriptions_and_offers/implementing_introductory_offers_in_your_app) and [App Store Promoted Purchases](app-store-promoted-purchases), which are used for new users and for promoting your products through the App Store, respectively. Promotional offers are useful for re-engaging former subscribers or keeping current ones on board. Consider offering a limited-time discount or a free trial period for auto-renewable subscriptions on macOS, iOS, and tvOS. This can encourage lapsed users to return and help retain your existing subscriber base. To use promotional offers in your app, you will need to complete a few steps: 1. Create a promotional offer in App Store Connect. 2. Connect your In-App Purchase API key in Qonversion. If you haven't set it up yet, follow the [In-App Purchase API Key](in-app-purchase-api-key) guide — it covers when, how, and where. 3. Use the new features of our SDK to obtain the promo offer and make a purchase with it. ### Create a promotional offer Add your Promotional Offer in App Store Connect. You can find the instructions in the [official documentation](https://developer.apple.com/help/app-store-connect/manage-subscriptions/set-up-promotional-offers-for-auto-renewable-subscriptions). ### Generate a Private Key Promotional offers are signed with the **In-App Purchase API key** saved under **Project Settings → Stores → Apple App Store** in Qonversion. If you haven't added one yet, follow the [In-App Purchase API Key](in-app-purchase-api-key) guide. ### Get a promotional offer and make a purchase The promotional offers you create in App Store Connect can be accessed within our SDK by calling `Qonversion.Product -> skProduct -> discounts`. You can determine whether the user is eligible for a promotional offer based on your product’s business logic. Once you decide to grant the promotional offer to a user, you need to call the following function: ```swift Swift theme={null} // You can obtain the product and discount in any other way. This approach is used here as an example. let mainProduct: Qonversion.Product = products["main"] let discount: SKProductDiscount? = mainProduct.skProduct?.discounts.first(where: { $0.identifier == "main_promo_offer" }) Qonversion.shared().getPromotionalOffer(for: mainProduct, discount: discount) { promoOffer, error in // show paywall with promo offer } ``` ```objectivec Objective-C theme={null} // You can obtain the product and discount in any other way. This approach is used here as an example. QONProduct *product = products[@"main"]; NSArray *discounts = [product.skProduct.discounts filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(SKProductDiscount *object, NSDictionary *bindings) { return [object.identifier isEqualToString:@"main_promo_offer"]; }]]; [[Qonversion sharedInstance] getPromotionalOfferForProduct:product discount:discounts.firstObject completion:^(QONPromotionalOffer * _Nullable promotionalOffer, NSError * _Nullable error) { // show paywall with promo offer }]; ``` ```dart Flutter theme={null} try { // You can obtain the product and discount in any other way. This approach is used here as an example. var promo = subscriptionProduct.skProduct?.discounts?.firstWhereOrNull( (discount) => discount.identifier == 'my_promo_offer_id' ); if (promo != null) { var promoOffer = await Qonversion.getSharedInstance().getPromotionalOffer(subscriptionProduct, promo); // handle promo offer here } } on Exception catch (e) { // handle error here } ``` ```typescript React Native theme={null} try { // You can obtain the product and discount in any other way. This approach is used here as an example. const promo = subscriptionProduct.skProduct?.discounts?.find(discount => discount.identifier === 'my_promo_offer_id' ); if (promo) { const promoOffer = await Qonversion.getSharedInstance().getPromotionalOffer(subscriptionProduct, promo); } // handle promo offer here } catch (e) { // handle error here } ``` ```csharp Unity theme={null} var promo = subscriptionProduct.SkProduct?.Discounts?.Find( discount => discount.Identifier == "my_promo_offer_id" ); if (promo != null) { Qonversion.GetSharedInstance().GetPromotionalOffer(subscriptionProduct, promo, (promoOffer, error) => { // Handle result here }); } ``` ```typescript Cordova theme={null} try { // You can obtain the product and discount in any other way. This approach is used here as an example. const promo = subscriptionProduct.skProduct?.discounts?.find(discount => discount.identifier === 'my_promo_offer_id' ); if (promo) { const promoOffer = await Qonversion.getSharedInstance().getPromotionalOffer(subscriptionProduct, promo); } // handle promo offer here } catch (e) { // handle error here } ``` ```typescript Capacitor theme={null} try { // You can obtain the product and discount in any other way. This approach is used here as an example. const promo = subscriptionProduct.skProduct?.discounts?.find(discount => discount.identifier === 'my_promo_offer_id' ); if (promo) { const promoOffer = await Qonversion.getSharedInstance().getPromotionalOffer(subscriptionProduct, promo); } // handle promo offer here } catch (e) { // handle error here } ``` After that, we will check on our server whether the user is eligible for this promotional offer. If they are, we will generate a signature for the purchase and return a `Qonversion.PromotionalOffer` object, which you will need to pass to the purchase function. #### Determine eligibility What availability conditions do we check for promotional offers? Those described in the [official documentation](https://developer.apple.com/documentation/storekit/in-app_purchase/original_api_for_in-app_purchase/subscriptions_and_offers/implementing_promotional_offers_in_your_app#3150971). In the future, these options may be expanded. We consider a user eligible for an offer if they have any active or expired subscription from any subscription group. #### Make a purchase To make a purchase with a promotional offer, take the object you received from the previous function, pass it into the purchase options object, and pass it to the `purchaseProduct` function. ```swift Swift theme={null} let purchaseOptions = Qonversion.PurchaseOptions(promoOffer: promoOffer) Qonversion.shared().purchase(product, options: purchaseOptions) { (result) in // handle purchase result here } ``` ```objectivec Objective-C theme={null} QONPurchaseOptions *purchaseOptions = [[QONPurchaseOptions alloc] initWithPromoOffer:promotionalOffer]; [[Qonversion sharedInstance] purchaseWithResult:product options:purchaseOptions completion:^(QONPurchaseResult * _Nonnull result) { // handle purchase result here }]; ``` ```dart Flutter theme={null} var purchaseOptions = QPurchaseOptionsBuilder() .setPromotionalOffer(promoOffer) .build(); var entitlements = await Qonversion.getSharedInstance().purchaseProduct( subscriptionProduct, purchaseOptions: purchaseOptions ); ``` ```typescript React Native theme={null} const purchaseOptions = new PurchaseOptionsBuilder() .setPromotionalOffer(promoOffer)) .build(); const entitlements = await Qonversion.getSharedInstance().purchaseProduct( subscriptionProduct, purchaseOptions ); ``` ```csharp Unity theme={null} var purchaseOptions = new PurchaseOptionsBuilder() .SetPromotionalOffer(promoOffer) .Build(); Qonversion.GetSharedInstance().PurchaseProduct( subscriptionProduct, purchaseOptions, (entitlements, error, isCancelled) => { ... }); ``` ```typescript Cordova theme={null} const purchaseOptions = new Qonversion.PurchaseOptionsBuilder() .setPromotionalOffer(promoOffer)) .build(); const entitlements = await Qonversion.getSharedInstance().purchaseProduct( subscriptionProduct, purchaseOptions ); ``` ```typescript Capacitor theme={null} const purchaseOptions = new PurchaseOptionsBuilder() .setPromotionalOffer(promoOffer)) .build(); const entitlements = await Qonversion.getSharedInstance().purchaseProduct( subscriptionProduct, purchaseOptions ); ``` *** [Migrate from SwiftyStoreKit](swiftystorekit-alternative) [Google Play Store](google-play) # Apple Ads Integration Source: https://documentation.qonversion.io/docs/apple-search-ads Connect Apple Ads with Qonversion, then collect attribution with the SDK. Qonversion combines Apple Ads reporting with attribution collected by the Qonversion SDK and Apple's AdServices framework. Connect your Apple Ads account first, then enable SDK collection. Read the [report guide](apple-ads-report) before comparing numbers with the Apple Ads console. ## 1. Connect with Apple 1. Open [Apple Ads settings](https://dash.qonversion.io/project-settings/asa) for your Qonversion project. Your dashboard role must allow you to manage the project's Apple Ads connection. 2. Select **Connect with Apple** and finish authorization in Apple. Choose the Apple Ads account you want this project to use. 3. Return to Qonversion and wait for the connection check. The page shows whether authorization is connected or still being verified. If it fails, use **Check again** or **Reconnect with Apple** on the same settings page. You don't need to create or upload a private API key. The status shows **Verifying** until Apple confirms access, then **Connected**. ### Projects connected with an API key If your project uses an Apple Ads API key, select **Migrate to Apple authorization** on the same page. Never send a private key or client secret to support or paste one into a chat. ## 2. Configure SDK ### 2.1 Enable Data Collection After you have completed the first three steps from the [Quick Start guide](quickstart) and have Qonversion SDK installed, enable data collection using the following SDK method: ```swift Swift theme={null} Qonversion.shared().collectAppleSearchAdsAttribution() ``` ```objectivec Objective-C theme={null} [[Qonversion sharedInstance] collectAppleSearchAdsAttribution]; ``` ```dart Flutter theme={null} Qonversion.getSharedInstance().collectAppleSearchAdsAttribution(); ``` ```typescript React Native theme={null} Qonversion.getSharedInstance().collectAppleSearchAdsAttribution(); ``` ```csharp Unity theme={null} Qonversion.GetSharedInstance().CollectAppleSearchAdsAttribution(); ``` ```typescript Cordova theme={null} Qonversion.getSharedInstance().collectAppleSearchAdsAttribution(); ``` ```typescript Capacitor theme={null} Qonversion.getSharedInstance().collectAppleSearchAdsAttribution(); ``` ### Tips 1. Run `collectAppleSearchAdsAttribution` method regardless of user App Tracking Transparency (ATT) response; it allows us to collect attribution in the most accurate way 2. Make sure you are using Qonversion iOS SDK version 2.18.2 or above ### 2.2 Import AdServices framework Starting with iOS 14.3, Apple Ads campaigns are measured through a new self-attributing integration based on the [AdServices framework](https://developer.apple.com/documentation/ad_services). Qonversion's Apple Ads attribution uses the Apple Ads Attribution API (AdServices), so it works independently of SKAdNetwork — no SKAdNetwork postbacks are needed. ### Done You've set up Apple Ads attribution. Attributed installs appear in the Apple Ads dashboard after users open your app, and campaign metrics appear as Apple reports them — Apple notes a newly activated campaign can take [24–48 hours](https://ads.apple.com/app-store/help/reporting/0007-tips-for-solving-performance-issues) to gather performance data. If the dashboard stays empty, check the connection in **Settings → Apple Ads**, confirm your app calls `collectAppleSearchAdsAttribution`, and widen the date range. Please contact support if you don't see the Apple Ads dashboard in your Qonversion account. *** [Apple Ads](apple-search-ads-analytics) [Analytics](analytics) # Apple Ads Source: https://documentation.qonversion.io/docs/apple-search-ads-analytics Attribution and analytics dashboard for Apple Ads tracking ## Apple Ads Analytics Overview Qonversion collects Apple Ads advertising attribution data so you can accurately measure the effectiveness of your campaigns and see exactly how much revenue each Apple Ads campaign and keyword generates. Qonversion Apple Ads tool tracks all critical metrics required for effective user acquisition on Apple's platform. Qonversion tracks in-app events, including trial conversions, subscription renewals, and purchases, alongside Apple Ads spend and delivery metrics. The report combines Apple Ads API data with Qonversion SDK and AdServices attribution. Qonversion's Apple Ads attribution uses the Apple Ads Attribution API (AdServices), so it works independently of SKAdNetwork — no SKAdNetwork postbacks are needed. Open **Analytics → Apple Search Ads** in the dashboard. The new Apple Ads report is available to accounts with it enabled — contact support; once it is enabled, select **Switch to Apple Ads** (or **Try the new report** in the banner) on that page and the report opens from **Apple Ads** in the sidebar. Connection settings are under **Settings → Apple Ads**. The [Apple Ads report guide](apple-ads-report) explains how to read the new report. In the Apple Ads report, choose Campaigns, Ad groups, or Keywords to inspect the same hierarchy. Select a period and revenue basis, then customize summary cards, the daily chart, and table columns. A campaign detail chart follows the selected entity. Make sure you have configured [Apple Ads integration](apple-search-ads) correctly. ## Available Metrics Metrics in the **Search Ads** source column, such as spend, impressions, taps, and Apple-reported installs, come from Apple's reporting API and require **Apple Ads Advanced**. Apple Ads Basic supports AdServices attribution, but does not provide keyword data or access to the reporting API. See [Apple's comparison of Basic and Advanced](https://ads.apple.com/app-store/help/apple-ads-basic/0001-compare-apple-ads-solutions). Apple Ads reporting can include the following metric families. Availability depends on the chosen level, date coverage, and data source; the report explains unavailable values in place. | Available Columns | Description | Source: Qonversion | Source: Search Ads | | - | - | - | - | | Ad Group Name | Apple Ads Advanced campaigns contain ad groups with defined bids and audience settings. Each campaign can be made up of one or more ad groups. | | ✅ | | ARPU | Average Revenue Per User. ARPU shows how much revenue a user brings to the company on average over a certain period. | ✅ | | | ARPPU | Average Revenue Per (Paying) User - the metric that considers only paying customers, as opposed to ARPU. | ✅ | | | Average CPA | Apple Ads campaign spend divided by Apple-reported tap-through installs in the selected reporting period. | | ✅ | | Average CPM | The average amount an Apple Ads Advanced advertiser pays per one thousand ad impressions on the App Store. | | ✅ | | Average CPT | The average amount an Apple Ads Advanced advertiser has paid for a tap on their ad. | | ✅ | | Impressions | The number of times your Apple Ads Advanced ad appeared on the App Store within a particular period. | | ✅ | | Installs | Apple-reported tap-through new downloads and redownloads in the selected reporting period. Apple uses a 30-day window from ad tap to download. | | ✅ | | Keyword | Someone may use a relevant word or term when searching for an app like yours. In your Apple Ads Advanced ad groups, you can add keywords your customers would search for on the App Store. | | ✅ | | New Downloads | App downloads from new users who have never before downloaded your app. | | ✅ | | Purchases | The number of purchases within a particular period. | ✅ | | | Redownloads | Downloads from users who previously downloaded and deleted your app then redownloaded it again or downloaded the same app on an additional device after tapping your ad on the App Store. | | ✅ | | Regular Subscriptions | The number of subscriptions started within a particular period. This field does not include subscriptions with promotional periods (Trial or Introductory). | ✅ | | | Revenue | The total revenue from users acquired during the period selected, including subscription renewals. | ✅ | | | Spend | In Apple Ads Advanced, the sum of the cost of each customer taps on your ad over a particular period. | | ✅ | | Taps | The number of times users tapped your Apple Ads Advanced ad within a particular period. | | ✅ | | Trials | The number of trials within a particular period. | ✅ | | | Trials converted | The number of started trial subscriptions that converted to paid subscribers. | ✅ | | | Trial to paid conversion | The percentage of started trial subscriptions that converted to paid subscribers. | ✅ | | | Users | The number of detected by Qonversion new users within a particular period. | ✅ | | | User to trial | The number of trial start events divided by the number of new users within a particular period. | ✅ | | | User to subscription | The percentage of new users that converted to a subscriber. | ✅ | | | Return on ad spend (ROAS) | The revenue ratio generated by an advertiser's investment in Apple Ads. Campaign (or app promotion) revenue divided by ad cost x 100. | ✅ | ✅ | The Apple **Installs**, **New Downloads**, and **Redownloads** columns use tap-through metrics. Apple also supports view-through attribution with a one-day window; those conversions are a separate metric. See [Apple's reporting definitions](https://ads.apple.com/app-store/help/reporting/0023-reporting-options-and-definitions). *** [Customer Details](customer-details) [Apple Ads Integration](apple-search-ads) # AppMetrica Source: https://documentation.qonversion.io/docs/appmetrica Send in-app subscription and purchases events to AppMetrica Qonversion sends [subscription events](integrations-overview#tracked-events) to AppMetrica to help you match your users' behavior with their payment history and inform your product decisions. ## 1. Setup the SDKs 1. Make sure you have AppMetrica SDK installed. More about the AppMetrica SDKs read [here](https://yandex.com/support2/appmetrica-io/en/sdk/platforms). 2. Set Qonversion SDK by following [installing the SDK guide](install-sdk). 3. To attribute sent from Qonversion events to a particular AppMetrica user, set AppMetrica Device ID Hash (`AppMetrica.deviceIDHash`): ```swift Swift theme={null} let completionBlock: (String?, Error?) -> Void = { deviceId, error in if let error = error { // Handle error here } else if let deviceId = deviceId { Qonversion.shared().setUserProperty(.appmetricaDeviceId, value: deviceId) } } // Specify the dispatch queue on which you want to execute the completion block, main queue as an example let queue = DispatchQueue.main // Use the main queue as an example YMMYandexMetrica.requestAppMetricaDeviceID(withCompletionQueue: queue, completionBlock: completionBlock) ``` ```objectivec Objective-C theme={null} YMMAppMetricaDeviceIDRetrievingBlock completionBlock = ^(NSString * _Nullable deviceId, NSError * _Nullable error) { if (error) { // Handle error here } else { [[Qonversion sharedInstance] setUserProperty:QONUserPropertyKeyAppMetricaDeviceId value:deviceId]; } }; // Specify the dispatch queue on which you want to execute the completion block, main queue as an example dispatch_queue_t queue = dispatch_get_main_queue(); [YMMYandexMetrica requestAppMetricaDeviceIDWithCompletionQueue:queue completionBlock:completionBlock]; ``` ```java Java theme={null} StartupParamsCallback startupParamsCallback = new StartupParamsCallback() { @Override public void onReceive(@Nullable Result result) { if (result != null) { // Note, that `deviceIdHash` is used, not `deviceId` Qonversion.getSharedInstance().setUserProperty(QUserPropertyKey.AppMetricaDeviceId, result.deviceIdHash); } } @Override public void onRequestError(@NonNull Reason reason, @Nullable Result result) { // ... } }; AppMetrica.requestStartupParams( this, startupParamsCallback, Arrays.asList( // Note, that `APPMETRICA_DEVICE_ID_HASH` is used, not `APPMETRICA_DEVICE_ID` StartupParamsCallback.APPMETRICA_DEVICE_ID_HASH ) ); ``` ```kotlin Kotlin theme={null} val startupParamsCallback = object : StartupParamsCallback { override fun onReceive(result: StartupParamsCallback.Result?) { // Note, that `deviceIdHash` is used, not `deviceId` result?.deviceIdHash?.let { Qonversion.shared.setUserProperty(QUserPropertyKey.AppMetricaDeviceId, it) } } override fun onRequestError( reason: StartupParamsCallback.Reason, result: StartupParamsCallback.Result? ) { // Handle error here } } AppMetrica.requestStartupParams( this, startupParamsCallback, listOf( // Note, that `APPMETRICA_DEVICE_ID_HASH` is used, not `APPMETRICA_DEVICE_ID` StartupParamsCallback.APPMETRICA_DEVICE_ID_HASH ) ) ``` ```dart Flutter theme={null} String deviceId = await AppMetrica.requestAppMetricaDeviceID(); Qonversion.getSharedInstance().setUserProperty(QUserPropertyKey.appMetricaDeviceId, deviceId); ``` ```typescript React Native theme={null} AppMetrica.requestAppMetricaDeviceID((deviceId, reason) => { if (deviceId) { Qonversion.getSharedInstance().setUserProperty(UserPropertyKey.APP_METRICA_DEVICE_ID, deviceId); } else { // Handle error here } }); ``` ```csharp Unity theme={null} AppMetrica.Instance.RequestAppMetricaDeviceID((deviceId, error) => { if (error != null) { // Handle error here } else { Qonversion.GetSharedInstance().SetUserProperty(UserPropertyKey.AppMetricaDeviceId, deviceId); } }); ``` 4. (Optionally) To improve attribution quality, set the same user ID to AppMetrica and Qonversion SDKs: ```swift Swift theme={null} YMMYandexMetrica.setUserProfileID("yourSideUserID") Qonversion.shared().setUserProperty(.appMetricaUserProfileId, value: "yourSideUserID") ``` ```objectivec Objective-C theme={null} [YMMYandexMetrica setUserProfileID:@"yourSideUserID"]; [[Qonversion sharedInstance] setUserProperty:QONUserPropertyKeyAppMetricaUserProfileId, value: @"yourSideUserID"; ``` ```java Java theme={null} AppMetrica.setUserProfileID("yourSideUserID"); Qonversion.getSharedInstance().setUserProperty(QUserPropertyKey.AppMetricaUserProfileId, "yourSideUserID"); ``` ```kotlin Kotlin theme={null} AppMetrica.setUserProfileID("yourSideUserID") Qonversion.shared.setUserProperty(QUserPropertyKey.AppMetricaUserProfileId, "yourSideUserID") ``` ```dart Flutter theme={null} await AppMetrica.setUserProfileID('yourSideUserID'); Qonversion.getSharedInstance().setUserProperty(QUserPropertyKey.appMetricaUserProfileId, 'yourSideUserID'); ``` ```typescript React Native theme={null} AppMetrica.setUserProfileID('yourSideUserID'); Qonversion.getSharedInstance().setUserProperty(UserPropertyKey.APP_METRICA_USER_PROFILE_ID, 'yourSideUserID'); ``` ```csharp Unity theme={null} AppMetrica.Instance.SetUserProfileID("yourSideUserID"); Qonversion.GetSharedInstance().SetUserProperty(UserPropertyKey.AppMetricaUserProfileId, "yourSideUserID"); ``` ## 2. Configure the AppMetrica integration 1. Navigate to your [AppMetrica dashboard](https://appmetrica.yandex.com/), select Settings and collect the following keys: 1. **Application ID**. It can be found right below the Application name in the General tab. 2. **Post API key**. It can be found in the General settings section as well. 2. Navigate to the [Integrations](https://dash.qonversion.io/app/integration/) section of your Qonversion project, select [AppMetrica](https://dash.qonversion.io/app/integration/create?name=appmetrica), provide the **Application ID** and **Post API key**, and click Save. 3. You can use the default event names provided by Qonversion or change them as you need. **→[Read more about tracked events here](integrations-overview#tracked-events)** ### Done Now Qonversion starts sending in-app purchases and subscription data to your AppMetrica account. ## Event Payload In case you need details about data sent to AppMetrica, follow the example below: ```json JSON theme={null} { "post_api_key": "your_post_api_key", "application_id": "your_application_id", "revenue_event_type": "example_event_type", "event_timestamp": "1707206995", "product_id": "example_product_id", "app_package_name": "com.example.app", "appmetrica_device_id": "8f65b16df378e7a6bece9614e1530fb55", "price": "19.99", "currency": "USD", "transaction_id": "example_transaction_id", "order_id": "example_order_id", "ios_ifa": "example_advertising_id_for_ios", "google_aid": "example_advertising_id_for_android", "os_name": "iOS", "os_version": "15.0", "device_model": "iPhone13,2", "device_locale": "en_US", "app_version_name": "1.0.0" } ``` *** [Amplitude](amplitude) [AppsFlyer](appsflyer) # AppsFlyer Source: https://documentation.qonversion.io/docs/appsflyer Send Qonversion in-app subscription events to AppsFlyer for accurate revenue tracking of trials, conversions, renewals, and refunds. Accurately measure what drives your subscription revenue on an ad campaign level by tracking events like trial-to-paying-user conversions, renewals, refunds, and upgrades. Qonversion tracks revenue even if a user does not open your app. This allows you to measure the performance of each subscription plan with 100% accuracy. ## 1. In your app: set up the SDKs 1. You need to have the AppsFlyer SDK integrated into your app before starting with this integration. If you do not have AppsFlyer integration yet, please use [this documentation](https://support.appsflyer.com/hc/en-us/articles/207032066-iOS-SDK-integration-for-developers). 2. Set Qonversion SDKs following [Installing the SDKs](install-sdk) guides. 3. Send AppsFlyer user ID to Qonversion via [User Properties](user-properties). In the **onConversionDataSuccess** callback use the`setProperty()` method with the AppsFlyer user ID value. ```swift Swift theme={null} // AppsFlyer 6 import AppsFlyerLib extension AppDelegate: AppsFlyerLibDelegate { func onConversionDataSuccess(_ conversionInfo: [AnyHashable : Any]!) { Qonversion.shared().setUserProperty(.appsFlyerUserID, value: AppsFlyerLib.shared().getAppsFlyerUID()) } func onConversionDataFail(_ error: Error!) { } } // Appsflyer 5 import AppsFlyerLib extension AppDelegate: AppsFlyerTrackerDelegate { func onConversionDataSuccess(_ conversionInfo: [AnyHashable : Any]!) { Qonversion.shared().setUserProperty(.appsFlyerUserID, value: AppsFlyerTracker.shared()?.getAppsFlyerUID()) } func onConversionDataFail(_ error: Error!) { } } ``` ```objectivec Objective-C theme={null} /// AppsFlyer 6 #import @interface AppDelegate: UIResponder - (void)onConversionDataSuccess:(NSDictionary *)conversionInfo { [[Qonversion sharedInstance] setUserProperty:QONUserPropertyKeyAppsFlyerUserID value:[[AppsFlyerLib shared] getAppsFlyerUID]]; } - (void)onConversionDataFail:(NSError *)error { } @end /// AppsFlyer 5 #import @interface AppDelegate: UIResponder - (void)onConversionDataSuccess:(NSDictionary *)conversionInfo { [[Qonversion sharedInstance] setUserProperty:QONUserPropertyKeyAppsFlyerUserID value:[[AppsFlyerTracker sharedTracker] getAppsFlyerUID]]; } - (void)onConversionDataFail:(NSError *)error { } @end ``` ```java Java theme={null} AppsFlyerConversionListener conversionListener = new AppsFlyerConversionListener() { @Override public void onConversionDataSuccess(final Map conversionData) { Qonversion.getSharedInstance().setUserProperty(QUserPropertyKey.AppsFlyerUserId, AppsFlyerLib.getInstance().getAppsFlyerUID(App.this)); } @Override public void onConversionDataFail(String errorMessage) { } @Override public void onAppOpenAttribution(Map conversionData) { } @Override public void onAttributionFailure(String errorMessage) { } }; AppsFlyerLib.getInstance().init("afDevKey", conversionListener, this); ``` ```kotlin Kotlin theme={null} val conversionListener: AppsFlyerConversionListener = object : AppsFlyerConversionListener { override fun onConversionDataSuccess(conversionData: Map) { Qonversion.shared.setUserProperty( QUserPropertyKey.AppsFlyerUserId, AppsFlyerLib.getInstance().getAppsFlyerUID(this@App) ) } override fun onConversionDataFail(errorMessage: String) {} override fun onAppOpenAttribution(conversionData: Map) {} override fun onAttributionFailure(errorMessage: String) {} } AppsFlyerLib.getInstance().init("afDevKey", conversionListener, this) ``` ```dart Flutter theme={null} _appsflyerSdk.onInstallConversionData((res) { print("res: " + res.toString()); setState(() { _gcd = res; }); Qonversion.getSharedInstance().setUserProperty(QUserPropertyKey.appsFlyerUserId, 'your appsflyer user id'); }); ``` ```typescript React Native theme={null} this.onInstallConversionDataCanceller = appsFlyer.onInstallConversionData( (res) => { appsFlyer.getAppsFlyerUID((err, appsFlyerUID) => { if (err) { console.error(err); } else { Qonversion.getSharedInstance().setUserProperty(UserPropertyKey.APPS_FLYER_USER_ID, appsFlyerUID); } }); } ); ``` ```csharp Unity theme={null} using AppsFlyerSDK; using QonversionUnity; public class AppsFlyerObjectScript : MonoBehaviour , IAppsFlyerConversionData { void Start() { /* AppsFlyer.setDebugLog(true); */ AppsFlyer.initSDK("devkey", "appID", this); AppsFlyer.startSDK(); } public void onConversionDataSuccess(string conversionData) { Qonversion.GetSharedInstance().SetUserProperty(UserPropertyKey.AppsFlyerUserId, AppsFlyer.getAppsFlyerId()); } } ``` ### Do not track any purchase events on the client side Qonversion tracks all revenue events so if you track revenue events with AppsFlyer SDK, you may double count the revenue in your AppsFlyer account. ## 2. In stores 1. Get your App ID. You can find the App ID in the App Store URL: 1. **For iOS** ``` https://apps.apple.com/us/app/apple-developer/640199958 ``` 2. **For Android** ``` https://play.google.com/store/apps/details?id=com.android.chrome ``` ## 3. In AppsFlyer 1. Get the AppsFlyer Dev Key in your AppsFlyer account, 1. Go to **Configuration → App Settings → Dev Key** and get it 2. Set up receiving events with S2S in the AppsFlyer 1. If your selected mode is SKAN 4 or Custom: 1. No settings changes are required. 2. If your selected mode is revenue, conversion, or engagement: 1. In AppsFlyer, go to Settings > SKAN Conversion Studio. 2. Click options (⋮). 3. Turn on Record in-app events sent by server-to-server API. 4. Send events by S2S. Read more: [https://support.appsflyer.com/hc/en-us/articles/4403727223185-SKAN-Conversion-Studio#report-events-by-servertoserver-api](https://support.appsflyer.com/hc/en-us/articles/4403727223185-SKAN-Conversion-Studio#report-events-by-servertoserver-api) ## 4. Configure the AppsFlyer integration on Qonversion's side ### Provide Integration Details 1. Navigate to the Integrations section in your Qonversion project, select [AppsFlyer](https://dash.qonversion.io/app/integration/appsflyer), and provide the Dev Key and App ID, and Save 1. Put in the Dev Key from the AppsFlyer 2. Put in App ID ### Configure the event names We recommend using the default event names provided by Qonversion. However, you can change the event names to suit your preferences. Note that the event names will not affect revenue recognition. All purchase events containing a value (subscription started, trial converted, subscription renewed, subscription refunded, in-app purchase) will be sent to AppsFlyer with the af\_revenue property, and the revenue will be recognized correctly in AppsFlyer. **→[Read more about tracked events here](integrations-overview#tracked-events)** ### Enable the integration ### Done! Now Qonversion will start sending in-app purchases and subscriptions data to your AppsFlyer account. ## Details ### About our event payload In case you need details about data sent to AppsFlyer, follow the example below: ```json theme={null} { "appsflyer_id": "", "os": "14.6", "app_version_name": "1.0.0", "customer_user_id": "outside_uid", "idfa": "", "idfv": "", "eventCurrency": "EUR", "ip": "", "bundleIdentifier": "com.app.name", "eventName": "trial_converted", "storefront": "USA", "eventTime": "Y-m-d H:i:s.u", "eventValue": { "af_content_type": "product", "af_content_id": "product_id", "q_uid": "QON_...", "af_revenue": "1.99", "af_currency": "EUR" } } ``` Notes: * `appsflyer_id` is generated by AppsFlyer. * `customer_user_id` is the unique user identifier set by the app owner. * `bundleIdentifier` is the app bundle ID from App Store Connect. * `eventTime` is UTC. AppsFlyer needs to receive each event by 02:00 UTC of the following day; otherwise AppsFlyer falls back to the time it received the event. ### Check out the sample Xcode Project [Here](https://github.com/qonversion/qonversion-ios-sdk/tree/develop/Sample) you can find the sample Xcode project that demonstrates the Qonversion and AppsFlyer integration ## How to compare your app revenue in AppsFlyer to Qonversion Navigate to your **AppsFlyer → Activity** dashboard. The date range relates to the event date and isn't LTV-based. The revenue you see in this dashboard is the revenue generated for the selected period. Revenue consists of in-app purchases (if reported) and ad revenue. To compare the total revenue from this dashboard to the revenue tracked by Qonversion: 1. Navigate to the Qonversion [Customers](http://dash.qonversion.io/customers/index) dashboard. 2. Select the same date range. 3. See the "Sales" figure at the top right. This sales figure should be equal to your revenue in AppsFlyer. Please note: 1. The sales data in Qonversion is before deducting app stores commissions. If you are sending the revenue to AppsFlyer net of App Store commission (step 1 (4) of this guide above), the sales number will be 15-30% higher in Qonversion than in AppsFlyer. 2. Qonversion does not track ad revenue 3. Qonversion Customers dashboard data does not contain one-off in-app purchases. There also might be a minor difference due to: 1. Time zones, Qonversion uses UTC time; 2. FX rates. *** What’s Next Other Attribution Platforms * [User Identifiers](user-identifiers) * [Amplitude](amplitude) * [Webhooks](webhooks) # App Store Offer Codes Source: https://documentation.qonversion.io/docs/appstore-offer-codes Implement Apple offer code redemption in your app using the Qonversion presentCodeRedemptionSheet API on iOS 14 and later. ## Redeeming Apple Offer Codes Users on iOS 14 and iPadOS 14 and later can redeem offer codes on the App Store through a one-time code redemption URL or within your app if you’ve implemented the `presentCodeRedemptionSheet` API. Apple recommends using offer codes directly in your app via a redemption sheet. Present the Offer Code redemption sheet to allow your users to redeem Offer Codes. ```swift Swift theme={null} Qonversion.shared().presentCodeRedemptionSheet() ``` ```objectivec Objective-C theme={null} [[Qonversion sharedInstance] presentCodeRedemptionSheet] ``` ```dart Flutter theme={null} Qonversion.getSharedInstance().presentCodeRedemptionSheet(); ``` ```typescript React Native theme={null} Qonversion.getSharedInstance().presentCodeRedemptionSheet(); ``` ```csharp Unity theme={null} Qonversion.GetSharedInstance().PresentCodeRedemptionSheet(); ``` ```typescript Cordova theme={null} Qonversion.getSharedInstance().presentCodeRedemptionSheet(); ``` ### Set up offer codes Set up the offer codes before testing. See the details on setting up offer codes [here](https://help.apple.com/app-store-connect/#/dev6a098e4b1). ## Handling Results If you need to get the results immediately after a user enters a promo code, please check the [deferred purchases](deferred-purchases) documentation. If a promo code was successfully applied, `checkEntitlements()` returns the updated entitlements. When a redeemed offer code applies to a Family Sharing-enabled subscription, the entitlement may be granted to a different family member. In that case `checkEntitlements()` on the redeeming device will not reflect the new entitlement. See [Apple Family Sharing](apple-family-sharing) for the family-aware entitlement model. *** [Apple App Store](apple-app-store) [App Store Privacy](app-store-privacy) # Asapty Source: https://documentation.qonversion.io/docs/asapty Send in-app subscription and purchases events to Asapty ## Requirements Before setting up the integration with Asapty, ensure that you have configured integration with [Apple Search Ads](apple-search-ads) and can view events attributed to this media source on the dashboard. ## Configure Integration 1. Navigate to the integrations page in Qonversion and select [Asapty](https://dash.qonversion.io/app/integration/create?name=asapty). 2. Find Asapty ID and provide it to Qonversion integration. 3. Configure Global Event Preferences: * Send Revenue Properties: Leave this enabled to receive revenue values. Disable only if you do not require revenue data. * Send Sales as Proceed: Choose whether to send revenue values net of App Stores’ commission (default) or gross. * Send Sandbox Events: Activate this toggle if you wish to receive sandbox events during testing. 4. Fine-Tune Event Settings: For each event, click More options to configure specific settings: * Enable Event: Toggle on to include the event in this integration. Turn it off if you do not need it. * Send Revenue: Enable or disable sending revenue for this particular event. 5. Save Your Configuration: Ensure that all settings are as desired and click Add new integration+/ Save to apply. ### Done Now Qonversion will start sending in-app purchases and subscriptions data to your Asapty account. ### Notes 1. You must repeat these steps for each app connected to Asapty. 2. Qonversion only supports sending In-App events from users attributed with Apple Search Ads. Install postbacks are not supported. *** [AppsFlyer](appsflyer) [Branch](branch) # Awards Source: https://documentation.qonversion.io/docs/awards Award badge templates showcasing App Store recognition and achievements. These components build trust by highlighting your app's accolades and editorial features. All Award templates support: * **App Store styling** — authentic Apple-inspired designs * **Full customization** — colors, text, images * **Multiple formats** — banners, tiles, and cards *** ## Wide Banners Full-width award banners for prominent placement. ### Apps You Need This Week A wide banner featuring the "Apps You Need This Week" App Store editorial style. **Features:** * Full-width banner layout * App Store editorial styling * App icon placeholder * "Apps You Need" branding **Use for:** * Hero sections on paywalls * Above-the-fold trust indicators * Featured app highlights *** ### App of the Day A wide banner showcasing "App of the Day" recognition. **Features:** * "App of the Day" title treatment * App icon and name display * Gradient background option * Premium visual styling **Use for:** * Main award showcase * Paywall hero sections * Trust-building headers *** ## Tiles Compact square award badges. ### App of the Day Tile A compact tile format for "App of the Day" recognition. **Features:** * Square tile format * App Store icon styling * Compact footprint * Date display option **Use for:** * Sidebar placements * Grid layouts * Compact trust indicators *** ## Sharp Cards Card-style awards with sharp corners and professional styling. ### Design Award Light Apple Design Award card with light theme. **Features:** * Light background * Apple Design Award styling * Year badge * Professional card design **Use for:** * Design-focused apps * Premium app presentations * Light-themed paywalls *** ### Design Award Dark Apple Design Award card with dark theme. **Features:** * Dark background * Apple Design Award styling * Contrast text treatment * Elegant dark mode design **Use for:** * Dark-themed apps * Premium presentations * Night mode paywalls *** ## Colored Banners Vibrant award banners with accent backgrounds. ### App of the Year A celebratory banner for "App of the Year" recognition. **Features:** * Bold accent background * "App of the Year" title * Premium visual treatment * High-impact styling **Use for:** * Major award highlights * Celebration screens * Premium upgrade prompts *** ### Apps You Need Yellow A vibrant yellow banner in the "Apps You Need" style. **Features:** * Bright yellow background * High contrast text * App Store editorial style * Attention-grabbing design **Use for:** * Promotional sections * Feature highlights * Eye-catching trust indicators *** ## Customization After adding an Award template, you can customize: **Content:** * Award title and subtitle * App name and description * Date or year display * App icon image **Styling:** * Background colors and gradients * Text colors and typography * Border radius and shadows * Icon size and positioning *** ## Best Practices * **Be authentic** — only display awards your app has actually received * **Use sparingly** — one prominent award badge is more effective than many * **Match context** — use App Store awards for iOS users, Play Store for Android * **Position strategically** — place near CTAs to boost conversion * **Keep it current** — highlight recent awards over older ones * **Combine with reviews** — pair awards with star ratings for maximum trust * **Respect guidelines** — follow Apple's and Google's trademark guidelines Ensure you have actually received the award before displaying it. Falsely claiming awards violates App Store guidelines and damages user trust. *** *** [Feature List](feature-list) [AI Image Generation](ai-image-generation) # Branch Source: https://documentation.qonversion.io/docs/branch Send iOS and Android in-app subscription events to Branch with Qonversion. Qonversion can automatically send every valuable mobile subscription event to your Branch account to help you measure the marketing performance. Measure what drives your revenue by tracking trial-to-paying-user conversion, subscription renewals, refunds, and other useful subscription events. ## 1. Set up the SDKs 1. Make sure you have Branch SDK installed. If you do not have Branch integration yet, please use this documentation for [iOS](https://help.branch.io/developers-hub/docs/ios-sdk-overview) and [Android](https://help.branch.io/developers-hub/docs/android-sdk-overview). 2. Set Qonversion SDKs following [installing the SDKs](install-sdk) guides. ## 2. Identify user **If your application includes user authentication**, it's important to link events sent from Qonversion with events received from the Branch SDK. This can be achieved by assigning the same user identifier to both Branch and Qonversion SDKs, as detailed in the [User Identifiers guide](user-identifiers#2-user-id-for-third-party-integrations). Below are the code snippets for various platforms to initialize the Branch SDK with the user ID: ```swift Swift theme={null} // Initialize Branch SDK with the user ID Branch.getInstance().setIdentity("yourSideUserID") // Logout Branch.getInstance().logout() ``` ```objectivec Objective-C theme={null} [[Branch getInstance] setIdentity:@"yourSideUserID"]; // Logout [[Branch getInstance] logout]; ``` ```java Java theme={null} Branch.getInstance().setIdentity("yourSideUserID"); // Logout Branch.getInstance().logout(); ``` ```kotlin Kotlin theme={null} Branch.getInstance().setIdentity("yourSideUserID") // Logout Branch.getInstance().logout() ``` ### Do not track any purchase events on the client side Qonversion tracks all revenue events so if you track revenue events with Branch SDK, you may double count the revenue in your Branch account. ## 3. Configure the Branch Integration ## Provide Integration Details 1. Get your **Branch Key** from [Branch Settings Dashboard](https://dashboard.branch.io/account-settings/app) 2. Navigate to the Integrations section in your Qonversion project, select [Branch](https://dash.qonversion.io/app/integration/branch), and provide the **Branch Key** and Save. ### Done Now Qonversion will start sending in-app purchases and subscriptions data to your Branch account. ## 4. Check the integration Qonversion sends all events as "Purchase" with a custom event alias to Branch. The event names that you set in Qonversion dashboards are the aliases. To see the events from Qonversion in Branch: * navigate to the Summary dashboard in Branch * filter PURCHASE events with the "show" filter * use the additional filter below the chart: "custom\_event\_alias" equals to the event name you set in Qonversion (e.g. trial\_started) ## 5. Countries events attribution Branch attributes all server events to the country where the server sending these events is located. The only way to change this and attribute events to users' countries is to contact Branch support. You should contact branch support with the request to whitelist your app (include your app's Branch ID in the communication) to record users' IP address instead of the server IP address. ## Event Payload In case you need details about data sent to Branch, follow the example below: ```json theme={null} { "branch_key":"key_live_KEY", "name":"PURCHASE", "customer_event_alias":"trial_converted", "user_data":{ "os":"", "developer_identity":"", "idfa":"" }, "event_data":{ "transaction_id":"", "currency":"EUR", "revenue":2.39 } } ``` **→[Read more about tracked events here](integrations-overview#tracked-events)** *** [Asapty](asapty) [Braze](braze) # Braze Source: https://documentation.qonversion.io/docs/braze Automatically send Qonversion in-app subscription and purchase events to Braze for customer engagement, messaging, and analytics. With Qonversion, you can automatically send in-app subscription and purchases events to Braze. ## 1. Set up the SDKs 1. Make sure you have Braze SDK installed. If you do not have Braze integration yet, please use this documentation for [iOS](https://www.braze.com/docs/developer_guide/platform_integration_guides/ios/initial_sdk_setup/) and [Android](https://www.braze.com/docs/developer_guide/platform_integration_guides/android/initial_sdk_setup/android_sdk_integration/). 2. Set Qonversion SDKs following [installing the SDKs](install-sdk) guides. 3. Attribute events sent from Qonversion and events received from the Braze SDK to the same user by setting the same user id to Braze that you set to Qonversion SDK in [User Identifiers guide](user-identifiers#2-user-id-for-third-party-integrations). ```swift Swift theme={null} Braze.shared.changeUser(userId: "yourSideUserID") ``` ```objectivec Objective-C theme={null} [braze changeUser:@"yourSideUserID"]; ``` ```java Java theme={null} Braze.getInstance(this).changeUser("yourSideUserID"); ``` ```kotlin Kotlin theme={null} Braze.getInstance(this).changeUser("yourSideUserID") ``` These snippets use the modern `Braze` class. If your app is still on the legacy `Appboy` SDK, the equivalent call is `Appboy.sharedInstance()?.changeUser("yourSideUserID")` - update to `Braze` when you bump the SDK. ## 2. Configure the Braze Integration ## Provide Integration Details 1. Get your **Braze API key** and your **Braze instance** from [Braze](https://dashboard-01.braze.com/). 2. Navigate to the Integrations section in your Qonversion project, select [Braze](https://dash.qonversion.io/app/integration/braze), and provide the **Braze API key** and your **Braze instance**, and Save. ### Done Now Qonversion will start sending in-app purchases and subscriptions data to your Braze account. ## Event Payload In case you need details about data sent to Braze, follow the example below: ```json theme={null} { "events":[ { "external_id":"638e041drd467600318edd46", "name":"subscription_renewed", "properties":{ "revenue":99, "storefront":"USA" }, "time":"2023-06-14T12:20:36+00:00" } ] } ``` *** [Branch](branch) [CleverTap](clevertap) # Capacitor Source: https://documentation.qonversion.io/docs/capacitor Install Qonversion Capacitor Plugin to validate user receipts, and get in-app subscription analytics and third-party integrations. [![GitHub release](https://img.shields.io/github/v/release/qonversion/capacitor-plugin?label=Latest%20Release)](https://github.com/qonversion/capacitor-plugin/releases) ## Install via npm Qonversion Plugin package is available on [npm](https://www.npmjs.com/package/@qonversion/capacitor-plugin). ```bash theme={null} npm install @qonversion/capacitor-plugin ``` After installing, sync the plugin into your iOS and Android projects so the native code is wired up: ```bash theme={null} npx cap sync ``` ## iOS native dependencies Starting from plugin version 1.9.0 the plugin ships both a `Package.swift` and a podspec, so `npx cap sync` resolves the native dependency (QonversionSandwich and the Qonversion iOS SDK) with whichever package manager your iOS project uses: * **Swift Package Manager** — the default for iOS projects created with Capacitor 8 (`npx cap add ios`). On Capacitor 7 create the project with `npx cap add ios --packagemanager SPM`. No CocoaPods installation is needed. * **CocoaPods** — iOS projects created with `npx cap add ios --packagemanager CocoaPods` on Capacitor 8, or with the default template on Capacitor 7 and older. `npx cap sync` runs `pod install` for you. The plugin supports Capacitor 7 and 8 (`@capacitor/core` 7.x or 8.x) and declares iOS 14.0 as its minimum deployment target. CocoaPods trunk becomes read-only on December 2, 2026: new versions of the native Qonversion SDK will no longer be published to CocoaPods trunk — the Swift Package Manager path is the supported way to receive them; existing CocoaPods builds keep working. To move an existing CocoaPods-based iOS project to Swift Package Manager, use Capacitor's assistant — `npx cap spm-migration-assistant` (it warns about plugins without a Swift package, see Capacitor's [Swift Package Manager guide](https://capacitorjs.com/docs/ios/spm)) — and see the [migration guide](dec-2026-migration-guide-cocoapods-to-spm). If your Podfile or `Package.swift` also declares the native `Qonversion` SDK directly, remove it: the sandwich pins the native SDK exactly, a second copy produces duplicate symbols or a package resolution conflict. ## Kids Mode for Qonversion Capacitor Plugin In case you are building an App for Kids, [follow this guide](kids-mode-sdk) to learn about Kids Mode for Qonversion SDKs. *** ## Next steps The SDK is installed. Choose the mode to implement — this determines how much code you write next. # Carousels Source: https://documentation.qonversion.io/docs/carousels Carousel templates provide swipeable, multi-slide layouts for showcasing content in an engaging, interactive format. Use carousels for onboarding flows, feature highlights, category navigation, and visual galleries. All Carousel templates support: * **Autoplay** — automatic slide advancement with configurable delay * **Touch navigation** — swipeable on mobile devices * **Loop mode** — infinite scrolling option * **Pagination** — optional navigation dots * **Extended overflow** — slides visible beyond container edges *** ### Tape Carousel A compact, tag-style carousel with icon and uppercase label pairs. Ideal for category navigation or feature highlights with a modern, minimal design. **Features:** * Icon + text tag design * Multiple slides visible simultaneously * Infinite loop enabled * No pagination dots * Compact, badge-like appearance **Use for:** * Category selection (e.g., Cycling, Swimming, Yoga) * Feature tags or highlights * Quick navigation options *** ### Gallery Carousel A full-width image carousel with large, bold headings overlaid on background images. Perfect for visual storytelling and immersive content presentation. **Features:** * Full-screen background images * Large heading text overlay * Internal pagination dots * Centered slide focus * Autoplay with smooth transitions **Use for:** * Onboarding flows with visual impact * Feature showcases with imagery * Brand storytelling sequences * Hero sections with rotating content *** ### Cards Carousel Colorful cards with titles displayed above each card, featuring images and descriptive text. Great for categorized content like age groups, skill levels, or product tiers. **Features:** * Title positioned outside (above) the card * Colored background containers * Image area with flexible sizing * Heading and description text * Multiple cards visible with peek effect * Equal height slides **Use for:** * Age group or tier selection * Category browsing with descriptions * Product or plan comparisons * Educational content organization *** ### Reviews Carousel A horizontal carousel displaying customer reviews with star ratings, testimonial text, and author names. Perfect for showcasing positive feedback and building trust before purchase decisions. **Features:** * 5-star rating display * Review text with automatic wrapping * Author name attribution * External pagination dots * Autoplay with 3-second default interval * Centered slides with peek effect **Use for:** * Testimonial sections on paywalls * Customer feedback highlights * Trust-building before purchase decisions Also available in [Social Proof](social-proof) category. *** ### Products Slider A horizontal carousel of product cards with optional badges, pricing, descriptions, and CTA buttons. Ideal for showcasing multiple subscription options in a swipeable format. **Features:** * Swipeable product cards * Optional "Best Value" or promotional badges * Price and description display * CTA button per product * Automatic select-product state handling * Autoplay with configurable timing **Use for:** * Multiple subscription tier comparison * Promotional offer showcases * Plan browsing with detailed cards Also available in [Product Blocks](product-blocks) category. *** ### Carousel Properties Reference | Property | Description | Default | | - | - | - | | **Slides Per View** | Number of slides visible at once | Varies by template | | **Space Between** | Gap between slides (px) | 15 | | **Autoplay Delay** | Seconds between transitions | 3 | | **Loop** | Enable infinite scrolling | Template-specific | | **Centered** | Center the active slide | Template-specific | | **Pagination** | Show navigation dots | Template-specific | | **Extended** | Show slides beyond container | Template-specific | *** ### Customization After adding a carousel template, you can customize: **Slides:** * Add or remove slides * Edit slide content (text, images, icons) * Adjust individual slide styling **Layout:** * Change slides per view * Adjust spacing between slides * Enable/disable centering **Behavior:** * Set autoplay speed or disable * Toggle loop mode * Show/hide pagination **Styling:** * Background colors * Typography (fonts, sizes, colors) * Border radius and shadows * Padding and margins *** ### Best Practices * **Limit slide count** — 3-5 slides optimal for engagement * **Use consistent styling** — maintain visual harmony across slides * **Keep content scannable** — users swipe quickly, make each slide count * **Test autoplay timing** — ensure users have time to read content * **Consider touch targets** — make interactive elements easy to tap * **Preview on devices** — test carousel behavior on different screen sizes *** *** [Social Proof](social-proof) [CTA Buttons](cta-buttons) # How to check user entitlements Source: https://documentation.qonversion.io/docs/check-permissions Check a user's entitlements with the checkEntitlements method of the Qonversion SDK to gate access to premium features, read the renewal state, and inspect the Entitlement and Transaction objects. An entitlement is access to the premium features of your application. Check a user's entitlements with the `checkEntitlements()` method to decide what premium content to unlock. Read more about creating and using entitlements in [Entitlements](entitlements). Call `checkEntitlements()` at app launch to see whether a user has the entitlement you require. The method validates the user's receipt and returns the current entitlements. Qonversion can also manage cross-platform entitlements through the [user Identity concept](user-identifiers#3-user-identity): for example, after a user subscribes in your iOS app, you can check the same entitlements in your Android or web apps. The Qonversion SDK [caches product and entitlement data](offline-sdk-mode), so entitlements are still available immediately when the internet connection is lost or the server is delayed. An entitlement object is returned only if the user has made a purchase, or you granted the entitlement manually through [the Customer tab](customers#edit-entitlements) or the Grant Entitlement API. Otherwise `checkEntitlements()` returns an empty result — an empty result means the user has no entitlements, not an error. ```swift Swift theme={null} Qonversion.shared().checkEntitlements { (entitlements, error) in if let error = error { // handle error return } if let premium: Qonversion.Entitlement = entitlements["premium"], premium.isActive { switch premium.renewState { case .willRenew, .nonRenewable: // .willRenew is the state of an auto-renewable subscription // .nonRenewable is the state of consumable/non-consumable IAPs that could unlock lifetime access break case .billingIssue: // Grace period: entitlement is active, but there was some billing issue. // Prompt the user to update the payment method. break case .cancelled: // The user has turned off auto-renewal for the subscription, but the subscription has not expired yet. // Prompt the user to resubscribe with a special offer. break default: break } } } ``` ```objectivec Objective-C theme={null} [[Qonversion sharedInstance] checkEntitlements:^(NSDictionary * _Nonnull entitlements, NSError * _Nullable error) { QONEntitlement *premiumEntitlement = entitlements[@"premium"]; if (premiumEntitlement && premiumEntitlement.isActive) { switch (premiumEntitlement.renewState) { case QONEntitlementRenewStateWillRenew: case QONEntitlementRenewStateNonRenewable: // QONEntitlementRenewStateWillRenew is state for auto-renewable purchases // QONEntitlementRenewStateNonRenewable is state for in-app purchases that unlock the entitlement lifetime break; case QONEntitlementRenewStateBillingIssue: // Grace period: entitlement is active, but there was some billing issue. // Prompt the user to update the payment method. break; case QONEntitlementRenewStateCancelled: // The user canceled the subscription, but the subscription has not expired yet. // Prompt the user to resubscribe with some special offer. break; default: break; } } }]; ``` ```java Java theme={null} Qonversion.getSharedInstance().checkEntitlements(new QonversionEntitlementsCallback() { @Override public void onSuccess(@NotNull Map entitlements) { final QEntitlement premiumEntitlement = entitlements.get("premium"); if (premiumEntitlement != null && premiumEntitlement.isActive()) { // handle active entitlement here // also you can check renew state if needed // for example to check if user has canceled subscription and offer him a discount switch (premiumEntitlement.getRenewState()) { case NonRenewable: // NonRenewable is the state of a consumable or non-consumable in-app purchase break; case WillRenew: // WillRenew is the state of an auto-renewable subscription break; case BillingIssue: // Prompt the user to update the payment method. break; case Canceled: // The user has turned off auto-renewal for the subscription, but the subscription has not expired yet. // Prompt the user to resubscribe with a special offer. break; default: break; } } } @Override public void onError(@NotNull QonversionError error) { // handle error here } }); ``` ```kotlin Kotlin theme={null} Qonversion.shared.checkEntitlements(object: QonversionEntitlementsCallback { override fun onSuccess(entitlements: Map) { val premiumEntitlement = entitlements["premium"] if (premiumEntitlement != null && premiumEntitlement.isActive) { // handle active entitlement here // also you can check renew state if needed // for example to check if user has canceled subscription and offer him a discount when (premiumEntitlement.renewState) { QEntitlementRenewState.NonRenewable -> { // NonRenewable is the state of a consumable or non-consumable in-app purchase } QEntitlementRenewState.WillRenew -> { // WillRenew is the state of an auto-renewable subscription } QEntitlementRenewState.BillingIssue -> { // Prompt the user to update the payment method. } QEntitlementRenewState.Canceled -> { // The user has turned off auto-renewal for the subscription, but the subscription has not expired yet. // Prompt the user to resubscribe with a special offer. } else -> { } } } } override fun onError(error: QonversionError) { // handle error here } }) ``` ```dart Flutter theme={null} try { final Map entitlements = await Qonversion.getSharedInstance().checkEntitlements(); final premium = entitlements['premium']; if (premium != null && premium.isActive) { switch (premium.renewState) { case QEntitlementRenewState.willRenew: case QEntitlementRenewState.nonRenewable: // .willRenew is the state of an auto-renewable subscription // .nonRenewable is the state of consumable/non-consumable IAPs that could unlock lifetime access break; case QEntitlementRenewState.billingIssue: // Grace period: entitlement is active, but there was some billing issue. // Prompt the user to update the payment method. break; case QEntitlementRenewState.canceled: // The user has turned off auto-renewal for the subscription, but the subscription has not expired yet. // Prompt the user to resubscribe with a special offer. break; default: break; } } } catch (e) { print(e); } ``` ```typescript React Native theme={null} try { const entitlements = await Qonversion.getSharedInstance().checkEntitlements(); const premiumEntitlement = entitlements.get('premium'); if (premiumEntitlement != null) { switch (premiumEntitlement.renewState) { case EntitlementRenewState.NON_RENEWABLE: // NON_RENEWABLE is the state of consumable/non-consumable IAPs that could unlock lifetime access break; case EntitlementRenewState.WILL_RENEW: // WILL_RENEW is the state of an auto-renewable subscription break; case EntitlementRenewState.CANCELED: // The user has turned off auto-renewal for the subscription, but the subscription has not expired yet. // Prompt the user to resubscribe with a special offer. break; case EntitlementRenewState.BILLING_ISSUE: // Grace period: entitlement is active, but there was some billing issue. // Prompt the user to update the payment method. break; case EntitlementRenewState.UNKNOWN: // We were unable to determine subscription renew state break; } } } catch (e) { // handle error here } ``` ```csharp Unity theme={null} Qonversion.GetSharedInstance().CheckEntitlements((entitlements, error) => { if (error == null) { if (entitlements.TryGetValue("premium", out Entitlement premium) && premium.IsActive) { switch(premium.RenewState) { case QEntitlementRenewState.WillRenew: case QEntitlementRenewState.NonRenewable: // .willRenew is the state of an auto-renewable subscription // .nonRenewable is the state of consumable/non-consumable IAPs that could unlock lifetime access break; case QEntitlementRenewState.BillingIssue: // Grace period: entitlement is active, but there was some billing issue. // Prompt the user to update the payment method. break; case QEntitlementRenewState.Canceled: // The user has turned off auto-renewal for the subscription, but the subscription has not expired yet. // Prompt the user to resubscribe with a special offer. break; default: break; } } } else { // Handle the error Debug.Log("Error" + error.ToString()); } }); ``` ```typescript Cordova theme={null} try { const entitlements = await Qonversion.getSharedInstance().checkEntitlements(); const premiumEntitlement = entitlements.get('premium'); if (premiumEntitlement != null) { switch (premiumEntitlement.renewState) { case Qonversion.EntitlementRenewState.NON_RENEWABLE: // NON_RENEWABLE is the state of consumable/non-consumable IAPs that could unlock lifetime access break; case Qonversion.EntitlementRenewState.WILL_RENEW: // WILL_RENEW is the state of an auto-renewable subscription break; case Qonversion.EntitlementRenewState.CANCELED: // The user has turned off auto-renewal for the subscription, but the subscription has not expired yet. // Prompt the user to resubscribe with a special offer. break; case Qonversion.EntitlementRenewState.BILLING_ISSUE: // Grace period: entitlement is active, but there was some billing issue. // Prompt the user to update the payment method. break; case Qonversion.EntitlementRenewState.UNKNOWN: // We were unable to determine subscription renew state break; } } } catch (e) { // handle error here } ``` ```typescript Capacitor theme={null} try { const entitlements = await Qonversion.getSharedInstance().checkEntitlements(); const premiumEntitlement = entitlements.get('premium'); if (premiumEntitlement != null) { switch (premiumEntitlement.renewState) { case EntitlementRenewState.NON_RENEWABLE: // NON_RENEWABLE is the state of consumable/non-consumable IAPs that could unlock lifetime access break; case EntitlementRenewState.WILL_RENEW: // WILL_RENEW is the state of an auto-renewable subscription break; case EntitlementRenewState.CANCELED: // The user has turned off auto-renewal for the subscription, but the subscription has not expired yet. // Prompt the user to resubscribe with a special offer. break; case EntitlementRenewState.BILLING_ISSUE: // Grace period: entitlement is active, but there was some billing issue. // Prompt the user to update the payment method. break; case EntitlementRenewState.UNKNOWN: // We were unable to determine subscription renew state break; } } } catch (e) { // handle error here } ``` ## The Entitlement object Each value in the `entitlements` map is an `Entitlement` object with the following fields. | Field | Type / values | Description | | - | - | - | | `id` | String | Qonversion entitlement ID. For example, `premium`. | | `isActive` | Boolean | `true` means the user has an active entitlement. `isActive = true` does not mean the subscription will renew — a user can have an active entitlement while auto-renewal is switched off. | | `source` | Enum: `appstore`, `playstore`, `stripe`, `paddle`, `manual`, `unknown` | Source via which the entitlement was activated: `appstore` — App Store; `playstore` — Play Store; `stripe` — Stripe; `paddle` — Paddle; `manual` — activated manually; `unknown` — source could not be detected. | | `startedDate` | Date | Initial transaction date. For a subscription with a trial period, this is when the trial starts. | | `trialStartDate` | Date, or `null` | The trial start date for the current entitlement. `null` for an entitlement unlocked by a consumable/non-consumable/lifetime purchase or a subscription without a trial. | | `firstPurchaseDate` | Date | The date of the first purchase. | | `lastPurchaseDate` | Date | The date of the last purchase. | | `autoRenewDisableDate` | Date | The date when auto-renew for the subscription was disabled. | | `expirationDate` | Date, or `null` | The expiration date for a subscription. `null` for a consumable/non-consumable in-app purchase or a lifetime subscription. | | `productId` | String | Identifier of the product from the Qonversion dashboard. | | `renewState` | Enum: `nonRenewable`, `willRenew`, `billingIssue`, `canceled`, `unknown` | Renewal state of the subscription: `nonRenewable` — consumable or non-consumable in-app purchase; `willRenew` — subscription is active and auto-renew is on; `billingIssue` — there was a billing issue; `canceled` — the subscription was canceled; `unknown` — no information about the renewal state. | | `renewsCount` | Integer | Subscription renews count for the entitlement. Counting starts from the second paid transaction. Example: with 20 transactions — the first is the trial-started transaction, the second is the first paid transaction (trial converted), and the remaining 18 are renewals, so `renewsCount` is 18. | | `grantType` | Enum: `purchase`, `familySharing`, `offerCode`, `manual` | How the entitlement was granted: `purchase` — the user bought a subscription; `familySharing` — via family sharing; `offerCode` — using an offer code; `manual` — via the Qonversion dashboard. | | `lastActivatedOfferCode` | String | The last activated offer code that unlocks the current entitlement. | | `transactions` | Array of `Transaction` | Transactions that unlocked the current entitlement. | ## The Transaction object Each element of the `transactions` array is a `Transaction` object with the following fields. | Field | Type / values | Description | | - | - | - | | `originalTransactionId` | String | The original transaction identifier. | | `transactionId` | String | The transaction identifier. | | `offerCode` | String | The offer code used to get the transaction. | | `transactionDate` | Date | The date of the transaction. | | `expirationDate` | Date, or `null` | The expiration date for the transaction. `null` for a consumable/non-consumable in-app purchase or a lifetime subscription. | | `transactionRevocationDate` | Date | The date the transaction was revoked. Set when the App Store refunds a transaction or revokes it from family sharing. | | `environment` | Enum: `sandbox`, `production` | The environment of the transaction. | | `ownershipType` | Enum: `owner`, `familySharing` | Ownership of the transaction: `owner` — the user owns the transaction; `familySharing` — the user got the transaction via family sharing. | | `type` | Enum: `subscriptionStarted`, `subscriptionRenewed`, `trialStarted`, `introStarted`, `introRenewed`, `nonConsumablePurchase` | The type of the transaction. | # CleverTap Source: https://documentation.qonversion.io/docs/clevertap Send in-app subscription events to CleverTap to engage and win back your subscribers Connect CleverTap account to Qonversion to launch user engagement campaigns, win back your customers, or reach them out when they have billing issues. After the connection is complete, Qonversion will be sending all the [subscription events](integrations-overview#tracked-events) to CleverTap. To run the integration, follow the steps below. ## 1. Setup the SDKs 1. Make sure you have CleverTap SDK installed. Check the official CleverTap SDK documentation [here](https://developer.clevertap.com/docs/clevertap-sdks). 2. Set Qonversion SDK following [Installing the SDKs guides](install-sdk). 3. Send *yourSideUserId* to Qonversion using [Custom User Properties](user-properties#optional-set-user-id). ## 2. Configure the CleverTap Integration 1. Open the CleverTap dashboard using your region-based URL and log in: * [India](https://in1.dashboard.clevertap.com/login.html) * [Singapore](https://sg1.dashboard.clevertap.com/login.html) * [United States](https://us1.dashboard.clevertap.com/login.html) * [Indonesia](https://aps3.dashboard.clevertap.com/login.html) * [Middle East (UAE)](https://mec1.dashboard.clevertap.com/login.html) * [Europe (default region)](https://eu1.dashboard.clevertap.com/login.html) 2. Navigate to the *Settings* page by clicking the gear icon in the bottom left navigation panel and selecting Settings dashboard. 3. Copy values from the *Project ID* and *Passcode* fields. We will need this information in the following steps. 4. Navigate to the *Tools → Integrations* section in your [Qonversion account](https://dash.qonversion.io/) 5. Choose your platform (IOS or Android), click the *Add new +* button, and select **CleverTap** 6. Provide the **Project ID** and **Passcode** copied during the fourth step above to the corresponding fields 7. Choose your CleverTap account region 8. Click **Save** or **Add new integration** 9. Congratulations! Now Qonversion will be sending all the [subscription events](integrations-overview#tracked-events) to your CleverTap account. ## Event Payload In case you need details about data sent to CleverTap, follow the example below: ```json theme={null} { "d":[ { "identity":"", //Unique user identifier set by the app owner "ts":1686891155, "type":"event", "evtName":"subscription_renewed", "evtData":{ "id":000000000, "platform":"iOS", "ip":"000.000.00.0", "product_id":"product_id", "quantity":1, "transaction_id":"2000000456329567", "country":"TR", "storefront":"USA", "app_version":"1.0", "os_name":"iOS", "os_version":"16.4.1", "device_model":"iPhone 11", "device_id":"", "revenue":1049.993, "currency":"TRY" } } ] } ``` *** [Braze](braze) [Facebook Ads](facebook) # Cohorts Source: https://documentation.qonversion.io/docs/cohorts Cohort analytics allows you to see a complete picture of how your subscriptions evolve over their lifetime In Qonversion, you can analyse your cohorts' progression using the following metrics: Revenue Retention and Subscriber Retention. ## Revenue ### 1. Net Revenue Retention * The first two columns show the month and the initial revenue (net of refunds) from customers who installed the app in the corresponding month. * Column 0 shows the initial cohort's revenue (net of refunds), calculated as a sum of Subscription Started and Trial Converted Event. * The following columns (1, 2, 3, etc.) represent the number of **consecutive renewal periods** since the customers subscribed (months for a monthly subscription, weeks for a weekly subscription, etc.). * The last column shows the total revenue from the corresponding cohort of users. ### 2. Subscriber Retention and ARPPU * The first two columns show the month and the initial number of paying customers who installed the app in that month. * Column 0 shows the initial number of customers who converted to paying after the app was installed. * The columns (1, 2, 3, etc.) represent the number of **consecutive renewal periods** since the customers subscribed (months for a monthly subscription, weeks for a weekly subscription, etc.). * The last column shows the ARP(P)U - Average Revenue Per (Paying) User. It is calculated as the cohort's cumulative revenue divided by the number of subscribers at its start. In other words, ARPPU is the actual historic LTV of your subscribers. ### 3. Include the Current Month Data You can choose to include or exclude the current month's data. If the current month switch is on, you will see the current month cohort data, and all previous cohorts will include the subscription renewals that happened during the current month. Since the current month is still ongoing, the last period's retention values typically will appear to be lower. That will also affect average retention numbers. *** [Events](events-analytics) [Integrations](integrations-overview) # Components Source: https://documentation.qonversion.io/docs/components Every screen in the No-Code Builder is made up of components — reusable, modular elements that define structure, content, and behavior. You can combine components freely to build anything from a single-page paywall to a multi-screen onboarding flow. Each component supports a unique set of properties, which can be configured in the Right Panel. Below you’ll find an overview of all available components, grouped by category, with quick links to their common use cases and key properties. *** ## Components Reference Table | Component | Parent | Actions | Description | Function | | - | - | - | - | - | | **Container** | ✅ | ✅ | Core layout block that groups and aligns other components. | Organizes and structures multiple elements | | **Button** | ✅ | ✅ | A clickable call-to-action element | Triggers a purchase attempt or other action | | **Product** | ✅ | ✅ | Displays a selectable product with customizable styling | Showcases in-app purchases or subscriptions | | **Heading** | ❌ | ❌ | Text block for titles using the /h1 tag | Adds main headings or key messages | | **Text** | ❌ | ❌ | Text block for descriptions, disclaimers, or variables using the /p tag | Adds body text, descriptions, or instructions | | **Toggle** | ❌ | ✅ | Switch element for selecting between two states (e.g., Monthly vs. Yearly) | Changes product selection | | **Tabs** | ✅ | ❌ | Segmented layout component for switching between different content states | Enables navigation between grouped sections | | **Tab** | ✅ | ❌ | Child element inside Tabs | Holds content for a single tab view | | **Badge** | ✅ | ✅ | Styled tag for emphasis (e.g., “Best Value,” “Limited Offer”). | Highlights products, offers, or section | | **Image/GIF** | ❌ | ❌ | Displays static or animated visuals | Adds images or GIFs to enhance visual design. | | **Icon** | ❌ | ✅ | Small decorative or functional icon element | Adds visual cues, or can be used as a **closing** button | | **Lottie** | ❌ | ❌ | Vector animation file with dynamic scaling | Adds lightweight animations or motion effects | | **Video** | ❌ | ❌ | Embeddable video player | Displays product demos or looping backgrounds | | **Slider** | ✅ | ❌ | Parent container that holds multiple Slides | Enables swipeable multi-page content | | **Slide** | ✅ | ❌ | Child component inside a Slider | Holds elements within each swipeable section | | **Quiz** | ✅ | ❌ | Parent container for quiz layouts with selectable options | Captures user input or preferences in onboarding flows | | **Star Rating** | ❌ | ❌ | Display-only star rating element with partial-fill support | Shows ratings on social-proof and review templates | | **Header** | ✅ | ❌ | Pre-built top section with standard layout and styling. | Used for consistent screen headers | | **Footer** | ✅ | ❌ | Pre-built bottom section with standard layout and styling. | Used for CTAs, legal text, or navigation. | **Parent Components** can contain other elements inside them. * Actions include functions like **Make Purchase**, **Open URL**, **Go to Page**, or **Select Product** and more. * Components can be freely combined to create custom layouts. *** ## Layout Components ### Container The **Container** is the foundational layout block in the **Builder**. It’s used to group and align multiple elements and can be nested inside other containers for flexible layout control. **Use cases:** * Creating columns, cards, or grouped sections * Holding text, buttons, and images together * Structuring responsive paywall layouts **Key properties:** * Action * Layout (direction, distribution, wrap, gap) * Alignment * Position (static, relative, absolute, fixed, sticky) * Size (fill, fit, auto, fixed) * Spacing (margin, padding) * Background, Border, Shadow * Selected State (if linked to product) **Tip:** Use containers as “cards” for product blocks or grouped pricing sections. *** ### Header / Footer (Pre-Built) Pre-designed layout sections that maintain consistent structure across screens. * **Header**: Ideal for app logos, titles, or progress indicators. * **Footer**: Perfect for CTAs, purchase buttons, or legal text. **Important:** * You can add only **one** **Header** and **one** **Footer** per screen. * Both are placed automatically at the top and bottom of the layout. * Their main property is **Positioning**, which is **Static** — they remain anchored in their respective places and do not scroll independently. **Key properties:** * **Alignment** – controls horizontal content placement (Start, Center, End). * **Padding / Background** – adjust internal spacing and visual styling. * **Typography** – inherit or override font family, weight, and color for text elements. * **Static Position** – ensures Header and Footer stay fixed to screen boundaries (not scrollable). *** ### Slider A horizontal parent container used to create swipeable multi-step experiences. **Use for:** * Onboarding flows * Multi-card offers or tutorials **Key properties:** * Direction (horizontal) * Slide count * Gap between slides #### Slide A single page inside a Slider. Each slide has independent content and styling. **Use for:** * Onboarding step * Product card or information screen **Key properties:** * **Background**, **Padding**, **Layout**, **Typography** *** ### Tabs A segmented layout component for switching between grouped content areas. **Use for:** * Switching between plans (Monthly, Annual, Lifetime) * Segmenting features or pricing groups **Key properties:** * Tab Items (add / rename / reorder) * Linked Containers or Product Groups * Default Tab * Orientation (horizontal / vertical) * Active / Inactive styles #### Tab A single view or section within the Tabs component. **Use for:** * Each pricing plan or content block linked to a tab selection. **Key properties:** * Layout * Padding / Margin * Background *** ## Text & Display Components ### Heading Large text element used for titles, section headers, or key messages. **Use for:** * Paywall headline (“Upgrade to Premium”) * Onboarding step titles **Key properties:** * Typography (font, weight, color, size and more) * Alignment * Margin / Padding * Variables (dynamic product data) *** ### Text Smaller text element used for supporting details or variable-based descriptions. **Use for:** * Feature lists * Descriptions and disclaimers * Dynamic price or period text **Key properties:** * Typography (font, color, size) * Alignment * Variables (e.g., products.product\_id.price\_per\_month) * Margin / Padding *** ### Badge A small highlighted label that draws attention to offers or key information. **Use for:** * “Best Value” or “Most Popular” tags * Promotional callouts **Key properties:** * Text content * Style (solid / outline / soft) * Color and background * Border radius * Alignment **Note**: **Badge** can be attached only to container-based components. *** ### Star Rating Display-only element that renders a configurable star rating with partial-fill support. Useful as a standalone trust indicator or as the visual primitive behind every review template - see the [Social Proof](social-proof) category for ready-made templates that use it. **Use for:** * Inline rating badges in headers * Trust indicators near CTAs * Custom social-proof layouts **Key properties:** * **Rating** — value from 0-5, supports decimals (e.g., 4.9 renders as 4 full stars + 1 star at 90% fill) * **Max Stars** — maximum number of stars to display * **Size** — star size in pixels * **Fill Color** — color for filled stars * **Empty Color** — color for unfilled stars * **Gap** — spacing between stars in pixels *** ## Interactive Components ### Button Clickable call-to-action element that triggers an event or navigation. Use for: * “Continue”, “Subscribe”, or “Buy Now” actions * Navigating between pages or screens Key properties: * Action (Make Purchase, Select Product, Open URL, Go to Page, Show Screen) * Label text (supports variables) * Size and alignment * Background, Border, Shadow * Typography (font, color, weight) *** ### Toggle Switch component that changes between two or more states, commonly used for pricing options. Use for: * Monthly ↔ Yearly plan switchers * Feature toggles **Key properties:** * Options (labels for each state) * Default State * Linked Products or Containers * Action on Change * Background and active/inactive text colors Combine Toggles with Product Containers to dynamically update user selection. ***Changing Products on toggles is coming soon*** *** ### Quiz Parent container that renders a list or grid of selectable options. Use it to capture user input during onboarding flows or to personalise the rest of a paywall based on the answer. **Use for:** * Onboarding questions ("What's your goal?", "How experienced are you?") * Preference capture before showing a tailored paywall * Multi-step quizzes that branch into different screens **Available templates:** * **Simple** — single-select list of plain options * **Multiple choice** — multi-select list * **Emoji** — list with leading emojis per option * **Icons** — list with leading icons per option * **Image tiles** — 2-column grid of image tiles Each Quiz contains child **Quiz Option** elements; selected options switch to a highlighted state automatically. **Key properties:** * Layout (list, image-tiles) * Template (simple, multiple-choice, emoji, icons, image-tiles) * Selected/default option styling * Background, Border, Padding * Linked navigation action on each option (Go to Page, Show Screen) *** ## Product Components ### Product A pre-built container displaying subscription or one-time purchase details. Automatically filled with data from your connected store. **Use for:** * Main product or subscription cards * Price comparison layouts **Key properties:** * Linked Product * Layout (vertical / horizontal) * Badge support * Product Variables (price, trial\_period, currency) * Background, Border, Shadow * Action (Select Product, Make Purchase) * Selected State styling **Tip**: Group multiple Product components in one Container for multi-plan layouts. *** ## Media Components ### Image / GIF Displays static or animated visual content. **Use for:** * Product illustrations * Onboarding artwork * Decorative visuals **Key properties:** * Source (upload or URL) * Fit (cover / contain) * Border radius / shadows * Alignment / Size * Action (optional, e.g., Open URL) *** ### Lottie File Vector-based animation file (.json) for smooth, scalable motion graphics. **Use for:** * Onboarding animations * Success or loading states **Key properties:** * Loop / Autoplay toggle * Playback speed * Alignment / Size *** ### Icon Simple decorative or functional symbol. **Use for:** * Feature checkmarks * Navigation or visual cues **Key properties:** * Icon set and name * Color and size * Alignment *** ### Video Embeddable video block for background loops or short product demos. **Available properties:** * Upload up to 150Mb * Autoplay / Loop toggle * Mute / Sound control * Cover fit *** #### Component Quick Reference | **Component** | **Purpose** | **Supports Actions** | **Has Children** | | - | - | - | - | | Container | Groups and aligns elements | ✅ | ✅ | | Header / Footer | Standard layout sections | ❌ | ✅ | | Slider | Swipeable parent layout | ❌ | ✅ | | Slide | Page inside slider | ❌ | ✅ | | Tabs | Switchable segmented view | ❌ | ✅ | | Tab | Individual tab section | ❌ | ✅ | | Heading | Large text element | ❌ | ❌ | | Text | Description or dynamic value | ❌ | ❌ | | Badge | Highlight tag | ✅ | ✅ | | Button | Interactive call-to-action | ✅ | ❌ | | Toggle | Multi-state switch | ✅ | ❌ | | Quiz | Selectable option container | ❌ | ✅ | | Star Rating | Display-only rating element | ❌ | ❌ | | Product | Pre-filled product card | ✅ | ✅ | | Image / GIF | Static or animated image | ❌ | ❌ | | Lottie | Animation file | ❌ | ❌ | | Icon | Decorative element | ✅ | ❌ | | Video | Embedded media | ❌ | ❌ | *** #### Best Practices * **Start simple:** use pre-built containers and headers to create clean layouts. * **Use variables** inside Text or Button components for dynamic pricing. * **Link product actions** early (Select Product, Make Purchase) to test your screen logic. * **Preview frequently**: test responsiveness on iOS, Android, and tablet devices. ### Prebuilt Components Prebuilt components are ready-to-use component templates designed to accelerate paywall creation. Instead of assembling individual elements, you can drop in a complete, styled product block and customize it to match your brand. *** Each template comes with: * **Pre-configured structure** with placeholder content * **Default and selected states** with automatic styling * **Professional design** following Qonversion best practices * **Full customization** — every property can be modified *** #### How to Add a Prebuilt Component 1. Open the **Left Sidebar** in the Builder. 2. Navigate to **Components** section. 3. Select a category (e.g., **Product Blocks**). 4. Browse the visual previews to find your preferred layout. 5. **Click** on a template to add it to your canvas. Prebuilt Components are ideal for quickly prototyping screens or getting started with proven layouts. You can always customize every aspect after adding them to your canvas. #### Product Selection States Product Block templates include automatic state-based styling: | State | Description | | - | - | | **Default** | Normal appearance when the product is not selected | | **Select Product** | Highlighted appearance when the user taps/selects the product | State changes are handled automatically when: * User clicks/taps a product in the live paywall * You preview the screen in the Builder **Visual indicators for selected state typically include:** * Border color change (purple accent) * Text color emphasis * Radio/checkbox fill * Background tint adjustment You can customize both states in the **Right Panel → States** section when a product container is selected. *** #### Customization After adding a prebuilt component, you have full control over every element: #### Styling * Colors (background, text, borders) * Typography (font family, size, weight) * Spacing (padding, margin, gap) * Border radius and shadows #### Content * Product titles and descriptions * Price display format * Badge text and positioning * Feature list items and icons #### Behavior * Linked products (connect to your actual store products) * Default selected product * Action on tap (Select Product, Make Purchase) Remember to link each product container to your actual Qonversion products before publishing. Templates use placeholder product IDs that need to be updated. *** #### Best Practices * **Start with a template** that matches your goals, then customize * **Link products early** — connect to real products before extensive styling * **Test both states** — preview how default and selected states look together * **Use badges strategically** — highlight your recommended plan with discount or "Best Value" badges * **Keep feature lists scannable** — 3–5 features per plan is optimal for mobile * **Preview on multiple devices** — use the device selector to check iOS, Android, and tablet layouts #### List of prebuilt components 1. [Product Blocks](product-blocks) - templates for subscription and purchase selection screens. 2. [Social Proof](social-proof) - customer reviews and testimonials to build trust. 3. [Carousels](carousels) - swipeable content layouts for onboarding, galleries, and navigation. 4. [CTA Buttons](cta-buttons) - ready-to-use call-to-action buttons with various styles. 5. [Feature List](feature-list) - feature lists, benefit overviews, and plan comparisons. 6. [Awards](awards) - App Store award badges and recognition displays. *** [Creating Screens](getting-started) [Product Blocks](product-blocks) # Conditional Logic Source: https://documentation.qonversion.io/docs/conditional-logic Dynamically change component styles based on device, user properties, product context, and custom variables Conditional logic allows you to dynamically change the appearance of any component on your paywall or onboarding screen based on runtime context — device info, selected product, user properties, or custom screen variables. For example, you can highlight a specific product's price in bold when the user selects it, hide an element on Android, or change the background color for dark mode users. Make sure your Qonversion SDK is updated to the latest version that supports conditional logic. See the [SDK Requirements](#sdk-requirements) section below. *** ## How It Works Each component on your screen has **style blocks** (Typography, Size, Spacing, Background, etc.). You can attach **conditional rules** to any style block. Each rule contains: 1. **Conditions** — when should this rule apply (e.g., "Platform equals iOS AND Theme equals dark") 2. **Style overrides** — what properties to change when the conditions are met (e.g., set font-size to 24px and color to white) At runtime, the Qonversion SDK provides the context (device info, user data, product details), and the conditional logic evaluator checks each rule top-to-bottom. The **first matching rule wins** — its overrides are applied, and subsequent rules are skipped. If no rule matches, the component keeps its default styles. *** ## Step-by-Step Example: Changing Typography for iOS Dark Mode Let's walk through a real example. We'll change the title text style when the user is on iOS in dark mode — applying a different font, size, weight, and color to ensure the text looks great on dark backgrounds. ### 1. Select the component Click on the text component you want to add conditional logic to. In this example, we select a title text on the paywall screen. ### 2. Open the Conditional Rules modal In the right sidebar, find the **Typography** style block. Hover over the block header — a lightning bolt icon will appear. Click it to open the Conditional Rules modal. ### 3. Add a new rule Click **Add Rule** at the bottom of the modal. A new empty rule appears with a condition group and a style overrides section. ### 4. Set the conditions We need two conditions combined with AND logic: **First condition:** * **Variable**: select `Platform` from the Device category * **Operator**: select `equals` * **Value**: select `iOS` **Second condition:** * **Variable**: select `Theme` from the Device category * **Operator**: select `equals` * **Value**: select `dark` This means: "Apply this rule when the user is on iOS AND using dark mode." ### 5. Set the style overrides In the **Overrides** section below the conditions, configure the typography changes: | Property | Value | | - | - | | Font family | `System UI` | | Font size | `22` px | | Font weight | `Semi Bold (600)` | | Color | `#f3e5e5` (light warm tone) | | Line height | `20` px | These values will override the component's default typography when both conditions are met. Conditional Rules modal with typography overrides ### 6. Save and preview Click **Apply** to save the rules. The lightning bolt on the Typography block now shows a badge with the rule count. To test it, open the **Preview Conditions Panel** (the test tube icon in the header). You can manually change the `Platform` and `Theme` variables to see how the component reacts in real time. *** ## Conditions In Detail ### Condition Groups: AND / OR Conditions within a rule are organized in **groups**. Each group has a logical operator: * **AND** — all conditions in the group must be true * **OR** — at least one condition must be true Click the `and`/`or` label between conditions to toggle the group operator. Groups can be **nested up to 2 levels deep**, allowing complex expressions like: ``` (Platform equals iOS AND OS Version greater_or_equal 16.0) OR (Platform equals Android AND OS Version greater_or_equal 13.0) ``` ### Available Variables #### Device | Variable | Type | Example values | | - | - | - | | Platform | String | `iOS`, `Android` | | OS Version | Version | `16.0`, `14.5.1` | | Language | String | `en`, `fr`, `de` | | Locale | String | `en_US`, `fr_FR` | | Theme | String | `light`, `dark` | | App Version | Version | `1.0.0`, `2.3.1` | | Country | String | `US`, `GB`, `DE` | #### Products | Variable | Type | Description | | - | - | - | | Selected Product | String | The product ID the user currently has selected | | Has Any Intro | Boolean | Whether any product has an introductory offer | | Per-product Has Intro | Boolean | Whether a specific product has an intro offer | | Per-product Intro Type | String | `free_trial`, `pay_as_you_go`, or `pay_up_front` | Product variables are generated dynamically based on the products configured for your screen. #### User | Variable | Type | Description | | - | - | - | | Entitlements | Array | List of user's active entitlements | | Has Any Entitlement | Boolean | Whether the user has at least one active entitlement | | Is First Launch | Boolean | Whether this is the user's first app launch | | Days Since Install | Number | Number of days since the app was installed | #### User Properties User properties set via the Qonversion SDK (both [defined and custom](user-properties)) are automatically available in the conditional logic context. They appear under the `user.properties` namespace. For example, if you set a custom user property `plan` with value `premium`, you can create a condition: * **Variable**: `user.properties.plan` * **Operator**: `equals` * **Value**: `premium` This allows you to personalize screens based on any user attribute you track — subscription tier, onboarding status, A/B test group, or any other custom property. User properties are loaded in parallel with other screen data, so they do not increase screen display time. #### Custom Screen Variables Custom screen variables are values you inject from your app code at runtime via the `CustomVariablesDelegate`. They are unique per screen (identified by `contextKey`) and are available for conditions under the custom variables namespace. For example, if your delegate returns `{"source": "settings"}`, you can create a condition: * **Variable**: `source` (from Custom Variables category) * **Operator**: `equals` * **Value**: `settings` See [Setting Up Custom Variables](#setting-up-custom-variables) below for implementation details. ### Available Operators | Operator | Works with | Description | | - | - | - | | equals | All types | Exact match (case-insensitive for strings) | | not equals | All types | Inverse of equals | | in | String, Version | Value is one of a comma-separated list | | not in | String, Version | Value is not in the list | | contains | Array, String | Array includes the value, or string contains substring | | does not contain | Array, String | Inverse of contains | | greater than | Number, Version | Numeric or semantic version comparison | | less than | Number, Version | Numeric or semantic version comparison | | greater or equal | Number, Version | Numeric or semantic version comparison | | less or equal | Number, Version | Numeric or semantic version comparison | | is empty | Array, String | Array has no elements, or string is empty/null | | is not empty | Array, String | Array has elements, or string is non-empty | Version comparisons (for OS Version and App Version) use semantic versioning — each segment is compared numerically. For example, `16.0` is greater than `9.3.6`. *** ## Rule Priority Rules are evaluated **top to bottom** within each style block. The first rule whose conditions evaluate to `true` is applied — all subsequent rules are skipped. This means you should order your rules from **most specific to least specific**: ``` Rule 1: Platform = iOS AND Theme = dark -> white text, large font Rule 2: Platform = iOS -> dark text, large font Rule 3: (default — no conditions match) -> keeps default styles ``` You can drag rules to reorder them in the Conditional Rules modal. *** ## Activating and Deactivating Rules Each style block has an **Active** toggle in the Conditional Rules modal header. When toggled off: * All rules are preserved (not deleted) * Rules are not evaluated at runtime * The lightning bolt indicator remains visible but rules have no effect This is useful for temporarily disabling conditional logic without losing your configuration. *** ## Testing with Preview Panel You don't need to publish your screen to test conditional logic. The No-Code Builder has a built-in **Preview Conditions Panel** that lets you simulate any combination of variables and instantly see how your rules affect the screen — right inside the editor. ### How to open the panel 1. Make sure you have at least one conditional rule configured on any component 2. Switch to **Preview** mode using the toggle in the builder header 3. The **Preview Conditions Panel** automatically appears on the left side of the screen The panel lists every variable used in your conditional rules across all components on the current screen. ### Testing variables Each variable has an appropriate input control depending on its type: * **Dropdowns** — for variables with predefined values (Platform, Theme, Selected Product, Intro Type, etc.). Just pick a value from the list * **Checkboxes** — for boolean variables (Has Any Intro, Has Any Entitlement, Is First Launch, etc.). Toggle on or off * **Text inputs** — for string and version variables (OS Version, Language, App Version, Country, etc.). Type any value to simulate * **Number inputs** — for numeric variables (Days Since Install, etc.) As you change values, the canvas **updates in real time** — you can see exactly which styles are applied and how the screen looks for any combination of conditions. ### Active Rules The bottom section of the panel shows **Active Rules** — a live list of which rules are currently matching based on the current variable values. This makes it easy to verify that the right rules fire for the right conditions. Preview Conditions Panel Try different combinations of variables to make sure your rules cover all the edge cases — for example, switching between iOS and Android while toggling dark mode, or selecting different products to verify the highlighting works correctly. *** ## Common Use Cases ### Highlight the selected product **Condition**: Selected Product equals the product ID **Typography override**: Bold font weight, accent color **Use case**: Make the currently selected product's title or price stand out visually. ### Platform-specific layout **Condition**: Platform equals `iOS` / `Android` **Spacing or Size override**: Adjust padding, margins, or dimensions **Use case**: Account for platform-specific UI conventions (e.g., larger touch targets on Android). ### Dark mode support **Condition**: Theme equals `dark` **Typography + Background + Border override**: Light text on dark background, adjusted border colors **Use case**: Ensure your paywall looks great in both light and dark themes. ### Show/hide elements by entitlements **Condition**: Has Any Entitlement equals `true` **Appearance override**: Visibility hidden **Use case**: Hide the purchase button for users who already have an active subscription. ### Free trial badge **Condition**: Product Has Intro equals `true` AND Intro Type equals `free_trial` **Appearance + Typography override**: Show a badge, change text color **Use case**: Visually indicate which products offer a free trial. ### First-time user experience **Condition**: Is First Launch equals `true` **Any style override**: Different layout, larger text, highlighted CTA **Use case**: Show a more prominent onboarding experience for first-time users. ### Personalization with custom variables **Condition**: Custom variable `plan` equals `free` **Typography + Appearance override**: Show upgrade messaging, accent CTA color **Use case**: Display a prominent upgrade prompt for free-tier users while hiding it for premium subscribers. Variables are injected via `CustomVariablesDelegate`. ### Context-aware styling with user properties **Condition**: `user.properties.loyalty_tier` equals `gold` **Background + Typography override**: Gold accent color, exclusive badge visibility **Use case**: Show VIP styling and exclusive offers to high-value users based on properties already tracked via the Qonversion SDK. *** ## Setting Up Custom Variables Custom Variables (and the `user.properties.*` namespace described above) are delivered to the screen context starting from the SDK versions listed in the [SDK Requirements](#sdk-requirements) table below. On earlier SDKs the rules are still embedded in the published HTML, but the corresponding values arrive empty and rules referencing them will not match. Custom screen variables let you pass dynamic data from your app into the No-Code screen context at runtime. This is useful for conditions that depend on app-specific state — such as the screen the user came from, their subscription tier, or feature flags. You provide variables by implementing a delegate that the SDK calls each time a screen is ready to display. The delegate receives a `contextKey` identifying the screen and returns a dictionary of string key-value pairs. Different screens can receive different variables. ### Implementation ```swift Swift theme={null} class MyVariablesDelegate: NoCodesCustomVariablesDelegate { func customVariables(for contextKey: String) -> [String: String] { // Return variables specific to this screen return [ "username": currentUser.name, "plan": currentUser.plan, "source": "settings" ] } } ``` ```kotlin Kotlin theme={null} class MyVariablesDelegate : CustomVariablesDelegate { override fun getCustomVariables(contextKey: String): Map { // Return variables specific to this screen return mapOf( "username" to currentUser.name, "plan" to currentUser.plan, "source" to "settings" ) } } ``` ```java Java theme={null} public class MyVariablesDelegate implements CustomVariablesDelegate { @Override public Map getCustomVariables(String contextKey) { // Return variables specific to this screen Map variables = new HashMap<>(); variables.put("username", currentUser.getName()); variables.put("plan", currentUser.getPlan()); variables.put("source", "settings"); return variables; } } ``` ### Setting the delegate You can set the delegate during initialization or at runtime: ```swift Swift theme={null} // During initialization let config = NoCodesConfiguration( projectKey: "projectKey", customVariablesDelegate: myDelegate ) NoCodes.initialize(with: config) // Or at runtime NoCodes.shared.set(customVariablesDelegate: myDelegate) ``` ```kotlin Kotlin theme={null} // During initialization val config = NoCodesConfig.Builder(this, "projectKey") .setCustomVariablesDelegate(myDelegate) .build() NoCodes.initialize(config) // Or at runtime NoCodes.shared.setCustomVariablesDelegate(myDelegate) ``` ```java Java theme={null} // During initialization NoCodesConfig config = new NoCodesConfig.Builder(this, "projectKey") .setCustomVariablesDelegate(myDelegate) .build(); NoCodes.initialize(config); // Or at runtime NoCodes.getSharedInstance().setCustomVariablesDelegate(myDelegate); ``` The delegate is called every time a screen is about to be shown, including nested screens during navigation within a NoCodes flow. Use the `contextKey` parameter to return different variables for different screens. ### How it works When the screen is ready to display, the SDK: 1. Calls your delegate with the screen's `contextKey` 2. Injects each returned key-value pair into the WebView JavaScript context via `window.noCodesSetVariable(name, value)` 3. The conditional logic evaluator uses these variables when checking rules ### Use cases for custom variables | Variable | Example value | Use case | | - | - | - | | `source` | `onboarding`, `settings` | Show different styles depending on where the user came from | | `plan` | `free`, `premium` | Highlight upgrade CTA for free users, hide it for premium | | `username` | `John` | Personalize greeting text via conditional visibility | | `ab_group` | `control`, `variant_a` | Apply different layouts per experiment group | | `loyalty_tier` | `gold`, `silver` | Customize offers based on loyalty status | *** ## SDK Requirements Conditional logic requires the following minimum SDK versions: | Platform | Conditional Logic | Custom Variables & User Properties | | - | - | - | | iOS | **6.9.0** | **6.11.0** | | Android (Qonversion SDK) | — | **9.4.0** | | Android No-Codes SDK | **1.7.0** | **1.9.0** | | Flutter | **11.4.0** | — | | React Native | **10.4.0** | — | | Unity | **9.3.0** | — | | Cordova | **7.3.0** | — | | Capacitor | **1.3.0** | — | The SDK provides runtime context to the paywall via the `setContext` event. If the SDK version is too old, conditional logic rules will still be present in the published HTML but the context will be empty — meaning no rules will match and default styles will be shown. Custom Variables and User Properties in the NoCodes context are currently available on iOS and Android native SDKs. Cross-platform SDK support will be added in future releases. *** ## Limitations * Conditional logic is available for **Mobile Paywall** and **Mobile Onboarding** screen types only * Condition groups can be nested up to **2 levels deep** * Each group supports up to **10 conditions** * A soft warning appears when a style block has **20+ rules** (no hard limit) * Rules are evaluated client-side in the published HTML — complex rule sets may have a minor performance impact * Custom variables from the delegate are string-only (`[String: String]`) — use string comparison operators in conditions * Custom variables are injected per screen show — they are not persisted across sessions * User properties must be set via the Qonversion SDK before the screen is shown to be available in conditions # Cordova Source: https://documentation.qonversion.io/docs/cordova Install Qonversion Cordova Plugin to validate user receipts, and get in-app subscription analytics and third-party integrations. [![GitHub release](https://img.shields.io/github/v/release/qonversion/cordova-plugin?label=Latest%20Release)](https://github.com/qonversion/cordova-plugin/releases) ## Install via Cordova Qonversion Plugin package is available on [npm](https://www.npmjs.com/package/@qonversion/cordova-plugin). Add it to your project with: ```bash theme={null} cordova plugin add @qonversion/cordova-plugin ``` This downloads the package from npm and wires up the native iOS and Android modules in one step. ## iOS requirements Starting from plugin version 7.10.0 the native iOS dependency (QonversionSandwich and the Qonversion iOS SDK) is resolved through Swift Package Manager on cordova-ios 8 and through CocoaPods on older cordova-ios: | cordova-ios | Native dependency | What you need | | - | - | - | | 8.x | Swift Package Manager (the plugin is installed as a Swift package) | The CocoaPods tool still has to be installed (see below) | | 6.x / 7.x | CocoaPods (`QonversionSandwich` pod) | CocoaPods installed; `deployment-target` 13.0 in `config.xml` | The iOS deployment target must be 13.0 or higher. cordova-ios 8 uses 13.0 by default; on cordova-ios 6/7 add the preference to your `config.xml`, otherwise CocoaPods refuses to install the pod: ```xml config.xml theme={null} ``` On cordova-ios 8 the CocoaPods tool must still be installed even though nothing is downloaded from it: cordova-ios keeps processing the plugin's CocoaPods declarations, writes a Podfile without any pods and runs `pod install`. CocoaPods trunk becomes read-only on December 2, 2026: new versions of the native Qonversion SDK will no longer be published to CocoaPods trunk — the Swift Package Manager path is the supported way to receive them; existing CocoaPods builds keep working. Move to cordova-ios 8 before then — see the [migration guide](dec-2026-migration-guide-cocoapods-to-spm). ### Troubleshooting * **`CocoaPods was not found`** during `cordova plugin add` or `cordova build` — install CocoaPods (`sudo gem install cocoapods` or `brew install cocoapods`). On cordova-ios 8, if the plugin was added while CocoaPods was missing, re-add the iOS platform (`cordova platform rm ios && cordova platform add ios`) rather than only the plugin, otherwise Xcode reports `Conflicting identity for cordova-plugin`. * **`The platform of the target ... (iOS 11.0) is not compatible with QonversionSandwich`** on cordova-ios 6/7 — add the `deployment-target` preference above and run `cordova platform rm ios && cordova platform add ios`. * **A `Qonversion` pod declared in your own Podfile** — remove it: the sandwich pins the native SDK exactly, a second copy produces duplicate symbols. ## Kids Mode for Qonversion Cordova Plugin In case you are building an App for Kids, [follow this guide](kids-mode-sdk) to learn about Kids Mode for Qonversion SDKs. *** ## Next steps The SDK is installed. Choose the mode to implement — this determines how much code you write next. # Cordova 4.+ migration guide Source: https://documentation.qonversion.io/docs/cordova-4-migration-guide Historical migration guide for upgrading the Qonversion Cordova SDK to major version 4.+: updated initialization, renamed methods, and revised purchase and entitlement APIs. ## Upgrading version Increase the dependency version in your package.json file to upgrade your Qonversion SDK to the latest ```json theme={null} "cordova-plugin-qonversion": "^4.0.0" ``` ## Renamings In this major version, we have renamed several entities and public methods to make our namings more clear. To make everything work just change all the occurrences from the left column of the table below to the new ones. | Before upgrade | After upgrade | | - | - | | `UserProperty` | `UserPropertyKey` | | `setUserProperty` | `setCustomUserProperty` | | `setProperty` | `setUserProperty` | ## User properties changes As mentioned above, we have renamed the`UserProperty` enum to `UserPropertyKey`. It was made to release the name `UserProperty` for a class, containing both the property key and its value. This class will be used in the new Qonversion method `userProperties`, which will return all the properties set for the current user. ```typescript Cordova theme={null} try { const userProperties = await Qonversion.getSharedInstance().userProperties(); userProperties.properties.forEach(userProperty => { console.log('User property - key:' + userProperty.key + ', value: ' + userProperty.value); }); } catch (e) { // handle error here } ``` `UserProperties` class returned as the successful result of `userProperties` contains several useful fields and methods to get all the types of properties you may want: | Field | Description | | - | - | | `properties` | List of all user properties | | `definedProperties` | Subset of all user properties, which were set using Qonversion-defined keys | | `customProperties` | Subset of all user properties, which were set using custom keys | | `flatPropertiesMap` | A flattened version of all user properties as a key-value map | | `flatDefinedPropertiesMap` | A flattened version of defined user properties as a key-value map | | `flatCustomPropertiesMap` | A flattened version of custom user properties as a key-value map | | Method | Arguments | Description | | - | - | - | | `getProperty` | `key` - string | Searches for a property with the given property `key` in all properties list | | `getDefinedProperty` | `key` - `UserPropertyKey` | Searches for a property with the given Qonversion-defined property `key` in the defined properties list. | The last change here is that we've enriched `UserPropertyKey` enum with a `CUSTOM` key to represent all the custom properties. *** [Unity 6.+ migration guide](unity-6-migration-guide) [Web 1.+ migration guide](web-1-migration-guide) # Cordova SDK 4.x to 5.x migration guide Source: https://documentation.qonversion.io/docs/cordova-5-migration-guide We've upgraded the Google Play Billing Library dependency on Android to version 6 in this release, which has led to major changes in the product and subscription management parts of our SDK. These are described below. ## Upgrading version Increase the dependency version in your *package.json* file to upgrade your Qonversion SDK to the latest ```json theme={null} "cordova-plugin-qonversion": "^5.0.0" ``` ## Product updates ### Qonversion and Google Play product link changes Google Play has recently upgraded the structure of their subscription products. We've written about it [in our blog](https://qonversion.io/blog/google-play-billing-library-5-0/). In short, they combined the same subscriptions with different durations into a single subscription with different base plans. In addition, they announced different kinds of offers to base plans, including free trials or introductory prices. In this release, we begin to support those changes. Previously Qonversion **subscription** products required a Google Play subscription identifier. Now you can also set a specific **base plan** for the subscription. Different base plans of the same subscription can be linked to different Qonversion products. Let's have a look at an example. Let's say you sell monthly and annual subscriptions in your app, both on Android and iOS. With the previous Qonversion SDK versions, you would specify two Qonversion products (`premium_monthly` and `premium_annual`), two App Store products (`premium_monthly_ios` and `premium_annual_ios`), and two Google Play products (`premium_monthly_android` and `premium_annual_android`). Then you would link those store products to Qonversion products as follows: `premium_monthly` - `premium_monthly_ios` and `premium_monthly_android`, `premium_annual` - `premium_annual_ios` and `premium_annual_android`. With the new Google Play Subscriptions structure, you can create only one subscription for Android - `premium_android` and two base plans for it (`monthly` and `annual`). With this done, you can now use a single subscription for both Qonversion products as follows: `premium_monthly` - `premium_monthly_ios` and `premium_android` with `monthly` base plan, `premium_annual` - `premium_annual_ios` and `premium_android` with `annual` base plan. ### Store details for the `QProduct` class Speaking of the SDK side - the first big part of the changes is the addition of the new `basePlanId` field to `Product` which can be set to the product via Qonversion Dashboard. We have deprecated the `Product.skuDetails` field, because `SkuDetails` contain information only for backward compatible base plans and are now deprecated in the Google Play Billing Library. Instead, we have added the new `storeDetails` field of type `ProductStoreDetails`, which contains all the store information for the Google Play Product. You can find the specifications of that class [on this page](google-play-product-details). If `basePlanId` is not specified for a product, it means one of the following: * it is a one-time (in-app) product that can not have any base plans, * it is a product that was not updated in the Qonversion dashboard after this version was released, * it is an iOS-only product. In the case of the in-app product, both `skuDetails` and `storeDetails` fields will contain information about that in-app. But in the second case, when there is a subscription product without a specified base plan, the `skuDetails` field will contain information about the **backward compatible** base plan, while the `storeDetails` field will only contain common subscription information without base-plan-specific data. It means that you should start using `storeDetails` field for base-plan-specific data (like price, duration, and so on) for Android subscription products only if you've specified their base plan IDs in Qonversion product settings. ### The other `Product` fields changes Along with the above changes, we have changed the `type` field and removed the `duration` and `trialDuration` fields from the product class. The `type` and `duration` were previously being set via the dashboard, and the `trialDuration` was calculated from the store details and was represented as an enumeration with several common durations. Now, the product `type` is calculated from the store details both for Android and iOS. We've also added the `INTRO` value to `ProductType` to separate `trial` and `intro` products (Android only for now). An `UNKNOWN` value was also added for the cases where we are unable to determine the product type. The `duration` and `trialDuration` fields were replaced with `subscriptionPeriod` and `trialPeriod`. Both properties are of type `SubscriptionPeriod` and the values are also calculated from the store details for both platforms. The last thing to note is that we've also changed the `prettyPrice` and `prettyIntrodactoryPrice` fields calculation for Android. Earlier they were calculated from `skuDetails`, while now we first try to use `storeDetails` if possible and only then fall back to `skuDetails`. ### `Product` updates summary Below is the shortened summary of the `Product` changes: * new `basePlanId` field, specifying concrete subscription base plan for Google Play subscriptions, added, * `skuDetails` field deprecated, * new `storeDetails` field, containing information about Google Play product, added; * the `type` field is now calculated based on store details both for Android and iOS, instead of the value set via the Qonversion dashboard. The `INTRO` and `UNKNOWN` values were added to `ProductType` to separate cases of trial and intro products (Android only for now) and also the case when we are unable to determine the product type; * the `duration` and `trialDuration` fields were replaced with `subscriptionPeriod` and `trialPeriod` fields of type `SubscriptionPeriod` with the values calculated from the store details both for Android and iOS; * the `prettyPrice` and `prettyIntroductoryPrice` fields on Android now prefers `storeDetails` to get the price from, and only then - `skuDetails`. ## Purchase flow updates In this release, we have changed the `purchase` and `updatePurchase` methods signatures. Previously there were several methods for different sets of arguments. Now we've arranged all those properties into classes - `PurchaseModel` and `PurchaseUpdateModel`, that should be instantiated and provided to the single `purchase` or `updatePurchase` method. You can create instances of those classes either manually or by using the utility functions `toPurchaseModel` and `toPurchaseUpdateModel` added to the `Product` class. Below is an example of how the `purchase` flow has changed: ```typescript Cordova theme={null} // Old const entitlements = await Qonversion.getSharedInstance().purchaseProduct(product); // New const purchaseModel = product.toPurchaseModel(); const entitlements = await Qonversion.getSharedInstance().purchase(purchaseModel); ``` And an `updatePurchase` flow example: ```typescript Cordova theme={null} // Old const entitlement = await Qonversion.getSharedInstance().updatePurchaseWithProduct(product, 'oldProductId'); // New const purchaseUpdateModel = product.toPurchaseUpdateModel('oldProductId'); const entitlement = await Qonversion.getSharedInstance().updatePurchase(purchaseUpdateModel); ``` If you were using only product identifiers, you can create purchase models manually as follows: ```typescript Cordova theme={null} // Old const entitlements = await Qonversion.getSharedInstance().purchase('productId'); // New const purchaseModel = new PurchaseModel('productId'); const entitlement = await Qonversion.getSharedInstance().updatePurchase(purchaseUpdateModel); ``` ```typescript Cordova theme={null} // Old const entitlement = await Qonversion.getSharedInstance().updatePurchase('newProductId', 'oldProductId'); // New const purchaseUpdateModel = new PurchaseUpdateModel('newProductId', 'oldProductId'); const entitlement = await Qonversion.getSharedInstance().updatePurchase(purchaseUpdateModel); ``` If necessary, you can provide a specific offer for subscription purchase. ```typescript Cordova theme={null} // Specifying offer via the `toPurchaseModel` method: const productOfferDetails = ...; // Choose an offer from `storeDetails` const purchaseModel = product.toPurchaseModelWithOffer(productOfferDetails); // Specifying offer ID via the `toPurchaseModel` method: const purchaseModel = product.toPurchaseModel('offer_id'); // Specifying offer ID via the constructor: const purchaseModel = new PurchaseModel('productId', 'offer_id'); const purchaseUpdateModel = new PurchaseUpdateModel('newProductId', 'oldProductId', 'offer_id'); // Specifying offer ID after the purchase model creation: purchaseModel.offerId = 'offer_id'; ``` If provided, we will try to find and purchase the offer with the specified ID for the requested Qonversion product. If there is no offer with the specified ID, an error will be returned. If no offer ID is provided for the subscription purchase of Qonversion product with a specified base plan ID, then we will choose the most profitable offer for the client from all the available offers. We calculate the cheapest price for the client by comparing all the trial or intro phases and the base plan. For iOS, old Qonversion products (where the base plan ID is not specified), or in-app products, the offer ID is ignored. You can also remove any intro/trial offer from the purchase (use only a bare base plan). For that purpose, you should call `removeOffer` method of purchase model: ```typescript Cordova theme={null} purchaseModel.removeOffer(); ``` ## The other changes * The `checkTrialIntroEligibility` method improved and now detects the eligibility based on store details on Android; * The `ProductDuration` and `TrialDuration` classes were removed; * The `ProductType` enum values were changed to meet guidelines; * The `ProrationMode` enum was removed and replaced with `PurchaseUpdatePolicy` ([read more](making-purchases)). *** [Unity SDK 6.x to 7.x migration guide](unity-7-migration-guide) [\[Aug 2023\] Migration Guide](qonversion-sdk-major-version) # Cordova SDK 5.x to 6.x migration guide Source: https://documentation.qonversion.io/docs/cordova-6-migration-guide We've upgraded the Google Play Billing Library dependency for Android to version 7 in this release, leading to several changes in our SDK. These are described below. ## Upgrading version Increase the dependency version in your *package.json* file to upgrade your Qonversion SDK to the latest ```json theme={null} "cordova-plugin-qonversion": "^6.0.0" ``` ## Android deployment upgrades With the new Google Play Billing Library 7 we've increased our `minSdkVersion` to 21 and `targetSdkVersion` to 34 for Android. If you were using lower `minSdkVersion` you should upgrade it to 21 to use the latest version of our SDK and Google Play Billing Library. If you have already targeted the 21+ version, you should do nothing. ## Installment plans support In the latest library version, Google introduces installment plans, when a customer commits to pay for several subsequent subscription periods. In our release, we've added a new field `installmentPlanDetails` to `ProductOfferDetails` with the details of the installment plan if they exist. It contains the following information: | Field | Type | Description | | - | - | - | | `commitmentPaymentsCount` | Int | Committed payments count after a user signs up for this subscription plan | | `subsequentCommitmentPaymentsCount` | Int | Subsequent committed payments count after this subscription plan renews | We've also added an `isInstallment` flag to `ProductStoreDetails` to check if the current product has an installment plan or not. Below is an example of those fields usage: ```typescript Cordova theme={null} const products = await Qonversion.getSharedInstance().products(); const installmentProduct = products['installmentProductId']; const storeDetails = installmentProduct?.storeDetails; if (storeDetails?.isInstallment) { const installmentDetails = storeDetails?.basePlanSubscriptionOfferDetails?.installmentPlanDetails; // Use the installment plan information } ``` ## Fallback files We're happy to introduce the fallback files support in this release to make our system reliability even higher. Fallback files allow your app to work as expected in rare cases of network connection or Qonversion API issues for new users without a cache available. This allows purchases and entitlements to be processed for new users even if the Qonversion API faces issues. This also makes it possible to receive remote configs for cases when the network connection is unavailable. Fallback files are available both on iOS and Android. Read more about the fallback files in [the documentation](system-reliability#fallback-files). ## Error codes In this release, we have added the `QonversionErrorCode` enum with the list of defined Qonversion error codes. Now you can compare any error code with the defined one to check any specific case as follows: ```typescript Cordova theme={null} try { await Qonversion.getSharedInstance().purchase(...); } catch (e) { if (e.code === QonversionErrorCode.STORE_PRODUCT_NOT_AVAILABLE) { // The requested product is unavailable for purchase. } } ``` We've also changed the type of `QonversionError.code` field from `string` to `QonversionErrorCode`. ## The other changes * Pending purchases support was added for prepaid subscriptions on Android. *** [Unity SDK 7.x to 8.x migration guide](unity-8-migration-guide) [\[Jan 2024\] Migration guide. Google Play Billing Library 6.](qonversion-sdk-major-version-copy) # Create Products Source: https://documentation.qonversion.io/docs/create-products Qonversion Product is a cross-platform entity representing products from Apple, Google, Stripe, or Paddle. This mapping helps you provide users with access to premium content after making a purchase. ## Create a Product 1. Navigate to the [Entitlements & Products configuration section](https://dash.qonversion.io/entitlements/products). Tap the Create button and select Product. 2. Fill in the product details. * **Qonversion Product ID** – create your unique product identifier in Qonversion that corresponds to a unique product on the Apple App Store and Google Play Store. The SDK will use it to make purchases. * **AppStore Product ID** – product identifier on Apple App Store. You can read [here](https://qonversion.io/blog/configure-iap-app-store-connect/) how to create a subscription. * **Google Play Product ID** – product identifier on Google Play Console. You can read [here](android-in-app-products) how to create a subscription. * **Google Play Base Plan ID** (shown as **Google Play Purchase Option ID** for one-time products) - for a Google Play subscription, the identifier of the base plan (required when your app uses the Qonversion Android SDK 9+ / Google Play Billing Library 8 — see the [migration guide](july-2025-migration-guide-google-play-billing-library-8)); for a one-time (in-app) product, the identifier of the [purchase option](https://developer.android.com/google/play/billing/one-time-products), which you can leave empty to use the product's default purchase option. Different base plans or purchase options of the same store product can be linked to different Qonversion products. * **Stripe Product ID** – the product identifier in Stripe, typically starting with `prod_`. Stripe also allows a [custom ID when a product is created through its API](https://docs.stripe.com/api/products/create#create_product-id). Copy the actual Product ID from Stripe; do not use a Price ID. See the [Stripe Integration](stripe-integration) guide. * **Paddle Product ID** – the [product identifier in Paddle Billing](https://developer.paddle.com/api-reference/products/get-product), starting with `pro_`. Use the Product ID, not the Price ID (`pri_`). See the [Paddle Integration](paddle-integration) guide. * **Associated Entitlements** - choose the entitlements that should be granted once this product is purchased. ### Change the promoted product in your app without releasing a new app version You can change the App Store products associated with the Qonversion product by changing the App Store Product IDs. *** [Create Entitlements](entitlements) [Displaying Products](displaying-products) # CTA Buttons Source: https://documentation.qonversion.io/docs/cta-buttons Call-to-action button templates designed to drive conversions. Each template includes a pre-styled button with optional supporting text, icons, and visual effects. All CTA Button templates support: * **Make Purchase action** — ready to connect to your products * **Full customization** — colors, fonts, shadows, gradients * **Responsive design** — adapts to different screen widths *** ### Simple Black A clean, minimalist black button with white text. Perfect for straightforward calls-to-action. **Features:** * Solid black background * Centered text label * Rounded corners * Hover/press state styling **Use for:** * Primary purchase buttons * Simple "Continue" or "Subscribe" actions * Minimalist paywall designs *** ### Extra Text A button with additional descriptive text below and stroke effect styling. **Features:** * Main action text * Supporting description text below * Stroke/outline effect * Two-line layout **Use for:** * Buttons with price or trial information * Actions that need additional context * Promotional CTAs with details *** ### Icon Gradient A vibrant button with an icon and gradient background for maximum visual impact. **Features:** * Customizable icon (left-aligned) * Gradient background * Bold text styling * Eye-catching visual treatment **Use for:** * Premium upgrade buttons * Feature unlock actions * High-conversion CTAs *** ### Two Line Shadow A two-line button with shadow effect and supporting text for detailed calls-to-action. **Features:** * Primary action text (large) * Secondary description text (smaller) * Drop shadow effect * Professional card-like appearance **Use for:** * Subscription buttons with pricing * Trial start actions with duration info * Premium CTAs with value proposition *** ## Customization After adding a CTA Button template, you can customize: **Visual styling:** * Background color or gradient * Text colors and typography * Border radius and shadows * Icon selection and color **Content:** * Main button text * Supporting description text * Icon choice **Behavior:** * Action type (Make Purchase, Open URL, Go to Page) * Linked product for purchases *** ## Best Practices * **Use action-oriented text** — "Start Free Trial" is better than "Submit" * **Include value proposition** — add pricing or benefit in secondary text * **Make it prominent** — CTA should be the most visible element * **Test different styles** — gradient buttons often outperform flat ones * **Position strategically** — place above the fold and after key benefits * **Use consistent styling** — match your app's brand colors *** *** [Carousels](carousels) [Feature List](feature-list) # Custom Actions Source: https://documentation.qonversion.io/docs/custom-actions Define your own actions in No-Code screens and handle them in your app code. ## Overview Custom Actions let you add your own handling for taps in a No-Code screen. Unlike built-in actions (purchase, restore, close, etc.), a Custom Action is not executed by the SDK — it is handed over to your app through the No-Codes delegate, and your code decides what happens. Each Custom Action is configured with a **value** — an arbitrary string that identifies the action, so your app code can tell which one was triggered. Use Custom Actions to run app-specific logic right from a No-Code screen, for example: * Open a native screen of your app * Start a support chat * Log a custom analytics event * Toggle an app feature *** ## Configuration To configure a Custom Action in the No-Code Builder: 1. Select a component 2. In the **Action** section of the right sidebar, choose **Custom action** 3. Enter the **Value** — the string your app code will use to identify this action When **Platform-Specific Actions** is enabled, you can configure different values (or entirely different actions) for iOS and Android independently. Custom Actions are available for mobile screens only. For Web Funnel screens the action is not offered in the builder — there is no SDK delegate on the web to receive it. *** ## Handling in Your App When a Custom Action is triggered, the SDK fires the standard action lifecycle callbacks (started → finished) and, in between, calls a dedicated delegate method. The configured value tells your code which action to run: ```swift Swift theme={null} extension MyClass: NoCodesDelegate { func noCodesReceivedCustomAction(value: String) { switch value { case "open_settings": // navigate to your native settings screen case "start_chat": // open your support chat default: break } } } ``` ```kotlin Kotlin theme={null} class MyActivity : AppCompatActivity(), NoCodesDelegate { override fun onCustomAction(value: String) { when (value) { "open_settings" -> { /* navigate to your native settings screen */ } "start_chat" -> { /* open your support chat */ } } } } ``` ```java Java theme={null} public class MyActivity extends AppCompatActivity implements NoCodesDelegate { @Override public void onCustomAction(@NonNull String value) { switch (value) { case "open_settings": // navigate to your native settings screen break; case "start_chat": // open your support chat break; } } } ``` Keep in mind: * The SDK performs no built-in handling — the whole behavior is up to your app code. * The screen stays open. Close it with `NoCodes.shared.close()` (iOS) / `NoCodes.shared.close()` (Android) if your custom logic requires it. * If no value was configured for the action, an empty string is delivered. On cross-platform SDKs, Custom Actions are delivered through the platform-specific listener: the optional `onCustomAction(value)` callback on React Native, Cordova, and Capacitor, the `customActionStream` on Flutter, and the optional `NoCodesCustomActionDelegate` interface on Unity. See [Custom action received event](displaying-no-codes#custom-action-received-event). *** ## Minimum SDK Versions Custom Actions require the following minimum SDK versions: | Platform | Minimum Version | | - | - | | iOS | 6.13.0 | | Android | No-Codes 1.10.0 | | React Native | 10.9.0 | | Flutter | 11.8.0 | | Cordova | 7.7.0 | | Capacitor | 1.6.0 | | Unity | 9.7.0 | ### **Note** Older SDK versions silently ignore Custom Actions — tapping the element does nothing, and no delegate callbacks are fired. *** [Success and Failure Actions](success-failure-actions) [Using No-Codes with custom purchases handling](using-no-codes-with-custom-purchases-handling) # Custom Proxy Server Source: https://documentation.qonversion.io/docs/custom-proxy-server-for-sdks Qonversion SDKs support Proxy URL settings. For example, you can use this feature for the following tasks 1. Log SDK requests on your server 2. Proxy SDK requests in case there are some network restrictions To leverage this feature, set your Proxy URL using the `setProxyURL` function. Once you implement it, all requests from our mobile SDK will be sent to your API, which should proxy that requests to our API and back. ```swift Swift theme={null} let config = Qonversion.Configuration(projectKey: "projectKey", launchMode: select_needed_launch_mode) config.setProxyURL("your_server_proxy_URL") Qonversion.initWithConfig(config) ``` ```objectivec Objective-C theme={null} QONConfiguration *configuration = [[QONConfiguration alloc] initWithProjectKey:@"projectKey" launchMode:select_needed_launch_mode]; [config setProxyURL:@"your_server_proxy_URL"]; [Qonversion initWithConfig:configuration]; ``` ```java Java theme={null} final QonversionConfig qonversionConfig = new QonversionConfig.Builder( this, "projectKey", select_needed_launch_mode ) .setProxyURL("your_server_proxy_URL") .build(); Qonversion.initialize(qonversionConfig); ``` ```kotlin Kotlin theme={null} val qonversionConfig = QonversionConfig.Builder( this, "projectKey", select_needed_launch_mode ) .setProxyURL("your_server_proxy_URL") .build() Qonversion.initialize(qonversionConfig) ``` ```dart Flutter theme={null} final config = new QonversionConfigBuilder( 'projectKey', select_needed_launch_mode ).setProxyURL('your_server_proxy_URL') .build(); Qonversion.initialize(config); ``` ```typescript React Native theme={null} const config = new QonversionConfigBuilder( 'projectKey', select_needed_launch_mode ).setProxyURL('your_server_proxy_URL') .build(); Qonversion.initialize(config); ``` ```csharp Unity theme={null} QonversionConfig config = new QonversionConfigBuilder( "projectKey", select_needed_launch_mode ).SetProxyURL("your_server_proxy_URL") .Build(); Qonversion.Initialize(config); ``` ```typescript Cordova theme={null} const config = new Qonversion.ConfigBuilder( 'projectKey', select_needed_launch_mode ).setProxyURL('your_server_proxy_URL') .build(); Qonversion.initialize(config); ``` ```typescript Capacitor theme={null} const config = new QonversionConfigBuilder( 'projectKey', select_needed_launch_mode ).setProxyURL('your_server_proxy_URL') .build(); Qonversion.initialize(config); ``` *** [Scheduled Reports](scheduled-reports) [Qonversion No-Code Builder](no-codes) # Customer Details Source: https://documentation.qonversion.io/docs/customer-details This tab contains a wealth of information about your customers and their interactions with your app or subscription business, such as the total amount spent, subscription events, status, and other attributes. The Qonversion ID and identity stay the same across sandbox and production, but the subscriptions, entitlement grants, customer history, devices, and revenue shown on this page are environment-scoped. Use the **Sandbox mode** icon in the top bar to switch which environment's data is loaded for the customer. ### Subscription widget The Subscription widget presents information about the user's last tracked subscription. | Field Name | Description | | - | - | | Status | The subscription status (Trial, Trial Cancelled, Trial Billing Retry, Active, Subscription Cancelled, Subscription Billing Retry, No Subscription). | | Started | The date the subscription was started. | | Last Payment | The date the last payment for this subscription was tracked. | | Subscription period | The subscription period (Weekly, Monthly, Yearly, etc.). | | Price | Subscription price in USD and original currency. | | Renewal date | The date when the subscription is expected to be renewed. | | Total | The total sum of payments made for the subscription and the number of payments. | | Store | The name of the store using which the subscription was made. AppStore, Google Play, or Stripe. | ### Overview The Overview widget helps you quickly look at your customers' main attributes. | Field Name | Description | | - | - | | Created At | The date the user was recognized by Qonversion and created. In other words, the first app launch date. | | Sales | The total sum of payments made by the user after deducting refunds. Consists of subscription and one-time payments. | | Payment Count | The total number of payments made by the user. Consists of subscription and one-time payments. | | Last Payment Date | The date the last payment from the user was tracked. | ### Entitlements The Entitlements widget displays entitlements granted to the user, both active and expired. | Field Name | Description | | - | - | | Entitlement name | The name of granted entitlement that previously was configured through [our dashboard](entitlements) for the [Subscription Management SDK mode](subscription-management-mode). | | Source | The source through which the entitlement was granted (App Store, Google Play, Stripe, Manual). | | Entitlement period | The period during which the entitlement was active. | ### Customer History The timeline with the customer's transaction events. Here you can find event names that [Qonversion tracks](integrations-overview#tracked-events) ### User Details | Field Name | Description | | - | - | | Qonversion ID | Unique user ID assigned by Qonversion. | | Device ID | The [identifierForVendor](https://developer.apple.com/documentation/uikit/uidevice/1620059-identifierforvendor) or [Settings Secure Android ID](https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID), depending on the user's platform | | Identity ID | Cross-device & cross-platform user identification you can set up by following [this guide](user-identifiers#3-user-identity). | | Country | User country collected by Qonversion SDKs. | | OS | The last OS name & version tracked by Qonversion SDKs. | | Model | Device model collected by Qonversion SDKs. | | App version | The last app version tracked. | | SDK version | The last Qonversion SDK version tracked. | | Created At | The date the user was recognized by Qonversion and created. In other words, the first app launch date. | | First Transaction Date | The date the first user event was tracked. | | First Payment Date | The date the first event with revenue was tracked. | | Last Payment Date | The date the last event with revenue was tracked. | *** [Edit Entitlements](edit-entitlements) [Apple Ads](apple-search-ads-analytics) # Customers Source: https://documentation.qonversion.io/docs/customers Analyze subscriber cohorts with metrics like active trials, subscriptions, churn rate, and payments for users acquired in a chosen date range. The Customers dashboard provides the data for a cohort of users who made payments in the selected period. For example, if you chose the Jan 1 to Jan 30 date range, you will get the metrics for the cohort or paying users acquired during this period based on their **subscription start date** or the **one-time purchase date**.