# TASK JOURNAL Source: https://documentation.qonversion.io/TASK_JOURNAL # Task Journal — WP-3.4 docs fact-check round 1 (pipeline P6) ## Task * **linear-id:** none * **track:** docs-improvement WP-3.4 (`../docs-improvement/PLAN.md` §5.4 P6, §5.2 map) * **title:** First fact-check pass over top funnel pages (Tier A, 16 pages) * **worktree:** documentation\_mintlify.worktrees/docs-factcheck (branch `docs/fact-check-r1`) * **skill:** `.claude/skills/docs-fact-check/SKILL.md` ## Goal For each of 16 Tier-A pages: extract checkable claims (SDK signatures/method names, version tables, API fields, limits, UI steps, stated negatives) → verify against the source-of-truth map (gh for SDK repos, local repos for backends/spec, rest-api-v4.yaml for API) → verdict confirmed/drift/unverifiable → record in `verification.json`. Only `verification.json` + this journal may be edited; content .mdx are owned by parallel agents — drifts go to the report only. ## Scope (16) quickstart, install-sdk, ios-sdk-setup, android-sdk, react-native-sdk, flutter-sdk, entitlements, create-products, user-properties, user-identifiers, testing, handling-errors, rate-limits, reference/v4/overview, reference/v4/authentication, snippets/code/init-sdk. ## Roadmap * [x] Read all 16 pages * [x] SDK version checks (releases): android 8.+ (DRIFT->9), flutter ^11 ok, RN pkg ok, ios ok * [x] init-sdk snippet: init method + LaunchMode enum all 8 platforms — CONFIRMED * [x] user-properties / user-identifiers / testing: method signatures + enums — CONFIRMED * [x] handling-errors: error-code enum table vs QONErrors.h — CONFIRMED (all Int values) * [x] rate-limits: 5 req/s vs android NetworkModule (=5) — CONFIRMED; version table unverifiable * [x] create-products: Android 9+/Billing 8 basePlanId CONFIRMED; Stripe/Paddle prefixes unverifiable (3rd-party) * [x] reference v4 overview + authentication vs rest-api-v4.yaml + api-gateway — CONFIRMED * [x] Write verification.json (16 entries) * [ ] Commit verification.json + journal ## Decisions * SDK repos not checked out locally → verify via gh (search code / release list). * Backends, mcp-server, spec are local under /Users/smejl/Desktop/github.com/qonversion/. * Version-table check first (cheapest, highest value per skill). ## Findings Drifts (report only — parallel agents own the .mdx): * **android-sdk.mdx (MEDIUM)**: install snippet `io.qonversion.android.sdk:sdk:8.+` but latest Android major is **9** (release sdk/9.6.0, 2026-07-21). Should be `9.+`. Contradicts create-products.mdx ("Android SDK 9+ / Billing Library 8"). * **user-properties.mdx (LOW)**: defined-property table omits SDK enum member `TenjinAnalyticsInstallationId('_q_tenjin_aiid')`; display name `AppMetricalProfileId` is a typo vs SDK `AppMetricaUserProfileId` (key matches). Unverifiable (surface to user, do not delete): * rate-limits.mdx per-platform "rate limit added in version" table (iOS 5.1.1, Android 6.1.1, RN 6.1.0, Flutter 7.1.0, Unity 6.1.0, Cordova 4.1.0, Cap 0.1.0) — historical intro versions, not walked per-repo this pass. * create-products.mdx Stripe `prod_` / Paddle `pro_` ID prefixes — third-party formats, outside our repos (correct per Stripe/Paddle conventions). * testing.mdx Apple/Google sandbox renewal-rate table — third-party console behaviour; Tier-A cadence is the only guard. * entitlements/create-products/testing dashboard UI steps — not walked in dash-mono this pass (low risk). Everything else CONFIRMED against source (see verification.json). ## Dead Ends * `gh search code` hit the 403 secondary rate limit mid-pass; switched to `gh api .../contents ... -H "Accept: application/vnd.github.raw+json"` (separate budget) to read whole SDK files. No pipes allowed → base64 decode via jq blocked; raw accept header avoids it. ## Next Action Fact-check follow-up fixes shipped on docs/factcheck-fixes (2026-07-25): android-sdk gradle pin 8.+ -> 9.+ (latest major 9.6.0), user-properties table (AppMetricaUserProfileId typo fixed + TenjinAnalyticsInstallationId row added), macos-catalyst wired to the NextStepsChooseMode snippet (the 8th SDK-setup page, missed by WP-3.1 ownership). Remaining: WP-3.2 screenshots (in flight). # 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). Valid public card codes: * `realtime` — today-so-far vs. yesterday counters (`trials_count`, `subscriptions_count`, `inapp_count`, `tracked_revenue`). Additional codes are internal-only and return 404 when requested publicly. # 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. Public 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`. - **Back-compat aliases** (factory maps to canonical executor): `user-to-trial-conversion` = `user-to-trial`, `subscription-cancellation-rate` = `subscription-cancellation`. Codes deferred to v4 api 2.0 (currently return 404): `cohort-revenue`, `cohort-active-subscriptions`, `experiment-new-users`, `experiment-users-to-trials`, `experiment-users-to-paid`, `refund-keeper`, `events`. 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. # 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. Manifest-compliant: list envelope, ISO 8601 timestamps. # 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 Regenerates the project's secret key. The old key is revoked; clients may see a short grace period (seconds) while internal caches expire. This is a destructive operation. Idempotency is intentionally NOT supported: caching a newly generated secret in a shared replay cache would leak credentials. Each call produces a fresh secret — clients must treat every request as unique. # 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 segment and percentage 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. Use `sandbox` environment for testing. Sandbox users do not affect production analytics. # 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 ## 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, rollout-percentage, 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. ## Rollout Percentage Is Now Enforced The rollout percentage (`segment_percent`) of a Remote Config now actually gates delivery: a user is admitted when their stable per-config bucket falls below the percent. The decision is **deterministic** — a config edit never reshuffles who is inside the rollout, and raising the percent only adds users. One heads-up: **drafts saved with a partial rollout come alive on activation.** A draft configured at, say, 43% will gate 43% of matching users the moment you activate it — review the rollout field of your drafts before flipping them on. See [How targeting and assignment works](remote-config-targeting) for the full evaluation model. ## 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. [Enable access to API here](https://console.cloud.google.com/flows/enableapi?apiid=pubsub). 2. Navigate to [project settings](https://dash.qonversion.io/project-settings/stores?tab=android), then click the "Connect to Google" button. 3. You need to copy generated **Google Developer Notifications Topic ID**. 4. Open the **Google Play Console** for your application, then navigate to **Monetization setup**. 5. In the **Real-time developer notifications** tab, insert your topic ID, click on **Save changes**, and then the **Send Test Notification** button. It takes up to 24 hours for your [Google service credentials](android-store-setup) to work correctly. Make sure to wait for at least 24 hours after you have created the credentials before connecting to Google Real-Time Developer Notifications. 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 ![](https://files.readme.io/placeholder-export.png) ### 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: ```bash bash 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:** ```bash bash theme={null} var config = NoCodesConfiguration( projectKey: "projectKey", locale: "de-DE" ) NoCodes.initialize(with: config) ``` ```bash bash theme={null} val config = NoCodesConfig.Builder(context, "projectKey") .setLocale("de-DE") .build() NoCodes.initialize(config) ``` ```bash bash theme={null} NoCodesConfig config = new NoCodesConfig.Builder(context, "projectKey") .setLocale("de-DE") .build(); NoCodes.initialize(config); ``` ```bash bash theme={null} const noCodesConfig = new NoCodesConfigBuilder("projectKey") .setLocale("de-DE") .build(); NoCodes.initialize(noCodesConfig); ``` ```bash bash theme={null} final noCodesConfig = NoCodesConfigBuilder("projectKey") .setLocale("de-DE") .build(); NoCodes.initialize(noCodesConfig); ``` ```bash bash theme={null} var noCodesConfig = new NoCodesConfigBuilder("projectKey") .SetLocale("de-DE") .Build(); NoCodes.Initialize(noCodesConfig); ``` ```bash bash theme={null} const noCodesConfig = new Qonversion.NoCodesConfigBuilder("projectKey") .setLocale("de-DE") .build(); Qonversion.NoCodes.initialize(noCodesConfig); ``` ```bash bash theme={null} const noCodesConfig = new NoCodesConfigBuilder("projectKey") .setLocale("de-DE") .build(); NoCodes.initialize(noCodesConfig); ``` **Setting locale after initialization:** ```bash bash theme={null} NoCodes.shared.setLocale("fr-FR") NoCodes.shared.showScreen(withContextKey: "yourContextKey") // Reset to system default NoCodes.shared.setLocale(nil) ``` ```bash bash theme={null} NoCodes.shared.setLocale("fr-FR") NoCodes.shared.showScreen("yourContextKey") // Reset to system default NoCodes.shared.setLocale(null) ``` ```bash bash theme={null} NoCodes.getSharedInstance().setLocale("fr-FR"); NoCodes.getSharedInstance().showScreen("yourContextKey"); // Reset to system default NoCodes.getSharedInstance().setLocale(null); ``` ```bash bash theme={null} NoCodes.getSharedInstance().setLocale("fr-FR"); NoCodes.getSharedInstance().showScreen("yourContextKey"); // Reset to system default NoCodes.getSharedInstance().setLocale(null); ``` ```bash bash theme={null} NoCodes.getSharedInstance().setLocale("fr-FR"); NoCodes.getSharedInstance().showScreen("yourContextKey"); // Reset to system default NoCodes.getSharedInstance().setLocale(null); ``` ```bash bash theme={null} NoCodes.GetSharedInstance().SetLocale("fr-FR"); NoCodes.GetSharedInstance().ShowScreen("yourContextKey"); // Reset to system default NoCodes.GetSharedInstance().SetLocale(null); ``` ```bash bash theme={null} Qonversion.NoCodes.getSharedInstance().setLocale("fr-FR"); Qonversion.NoCodes.getSharedInstance().showScreen("yourContextKey"); // Reset to system default Qonversion.NoCodes.getSharedInstance().setLocale(null); ``` ```bash bash 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 Implement subscription analytics in 20 minutes without changing your existing in-app purchases flow. Analytics (Observer) mode allows you to get the best-in-class analytics in minutes. 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. You don't need to change your current in-app purchases flow. After you install the SDK, follow the steps below. ## 1. Launch SDK Initialize the SDK in Analytics mode: **→[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). # Refund Keeper Analytics Overview Source: https://documentation.qonversion.io/docs/analytics-overview Refund Keeper doesn't just automate refund defense - it gives you full visibility into your refund activity and savings, all in one place. Here's what you'll find inside the **Refund Keeper** Analytics section. For general subscription analytics, see [Subscriptions](/docs/subscriptions) and [Events](/docs/events-analytics). ## Analytic Widgets ### Requested Refunds * Total number of refund requests submitted by users * Total dollar value (\$) of requested refunds ### Won Back Refunds * Number of refund requests successfully declined (blocked) * Total dollar value (\$) of revenue saved from declined refunds ### Won Back Refunds Rate * The percentage of refund requests Refund Keeper helped block. * Calculated as `(Won Back Refunds / Requested Refunds) × 100`. *** ## Refund Activity Timeline Chart * A dynamic timeline of refund requests and refunds won back over a selected period ## Detailed Refund Table A table listing every refund request, with the following columns: * **UID** - Qonversion user ID * **Transaction id** - the platform transaction identifier * **Product** - the product purchased * **Status** - one of `Pending`, `Refunded` (refund approved by the store), or `Saved` (Refund Keeper successfully blocked the refund) * **Price** - the refund amount, displayed in your selected currency * **Date Created** - when the refund request was submitted You can filter the table by tab (All / Pending / Refunded / Saved) and search by UID or transaction ID. ### Important Notes *** 1. **Refund Keeper** Analytics only tracks refund events where Refund Keeper was active. 2. Currently available for App Store refunds (Google Play refunds are not supported). 3. Requires Qonversion **Growth** or **Enterprise** Plan and **Server Notifications V2** enabled. **Refund Keeper** makes it easy to see not just that you're saving revenue—but exactly how, where, and when you're doing it 🚀 *** [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)**. The wizard surfaces both as a single step; this guide walks through generating each piece on the Google side before pasting them in. The flow spans two consoles — **Google Cloud Console** (sections 1–5) and **Google Play Console** (section 6) — and ends back in **Qonversion Dash** (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 Three Google APIs need to be enabled on the same Cloud project: * **Google Play Android Developer API** — lets Qonversion validate purchases and read subscription state. * **Google Play Developer Reporting API** — surfaces RTDN delivery health in the Qonversion settings card. * **Cloud Pub/Sub API** — required to provision the Real-Time Developer Notifications topic. ### 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 two APIs. ### 2.3 Enable each API On every Product details page, click **Enable**. You'll repeat this for all three 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, add two roles, then click **Done**: * **Pub/Sub Admin** — required to provision and manage the RTDN topic. * **Monitoring Viewer** — lets Qonversion surface RTDN delivery health on the settings card. ## 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 you lose it, delete the existing key from the Keys tab and generate a new one — the previous one stops working as soon as you revoke it. ## 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. Upload the `.json` key file you downloaded in section 4. 3. Click **Connect to Google** to provision the RTDN Pub/Sub topic. Qonversion shows the topic name in the same row — copy it and paste it in Google Play Console → **Monetization setup → Real-time developer notifications**. See [Google Developer Notifications](google-developer-notifications) for the Play Console step. 4. Click **Save**. ## 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. ### RTDN tag stays "Awaiting first notification" after a few minutes * Trigger a sandbox subscription event (renewal, refund) — Google only sends notifications for actual events. * Make sure **Pub/Sub Admin** is set on the service account; without it, Google can't publish to the topic. * Check that **Cloud Pub/Sub API** is enabled in the same Google Cloud project. ### Lost the .json key The Service Accounts → Keys tab lets you delete the old key and generate a new one. Re-upload the new file in the wizard — Qonversion will overwrite the stored credentials. *** ## 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 create an App Store Connect API key and connect it to Qonversion so we can sync your product catalog and read your subscription grace period configuration. The **App Store Connect API key** lets Qonversion read your app's metadata directly from App Store Connect. We use it to: * **Auto-sync your in-app purchase catalog** so you don't have to mirror every product change in Qonversion by hand. * **Read your subscription grace period configuration** so revenue and proceeds are attributed correctly when subscribers fall into a grace period. * **Auto-detect your Bundle ID and App Store ID** during the onboarding wizard. This is a **different key** from the [In-App Purchase API Key](in-app-purchase-api-key), which Qonversion uses for runtime transaction validation, refund detection, and subscription status. They're generated under different tabs in App Store Connect and they are **not interchangeable**. ## 1. Make sure you have the right role To generate an App Store Connect API key, your App Store Connect user must have one of these roles: * **Account Holder** * **Admin** * **App Manager** — with access to every app you want Qonversion to manage Contact your team's Account Holder if you don't have one of these roles. ## 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 **Admin** or **App Manager** (App Manager is enough; Admin works too). 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 the **Key Identifier** in Qonversion. * Above the table, copy the **Issuer ID** (UUID format, e.g. `57246542-96fe-1a63-e053-0824d011072a`) — this is the **Issuer Identifier** in Qonversion. * In the same row, click **Download API Key** to get the **.p8** file. Apple lets you download the `.p8` file **only once**. If you lose it, you have to revoke the key and create a new one. Save it somewhere safe before continuing. ## 5. Paste everything into Qonversion 1. Open Qonversion Dash → **Project Settings** → **Stores** → **Apple App Store**. 2. Click **Configure App Store** (or **Edit configuration** if you've already started). 3. On **Step 1 · Identify your app**: * Paste the **Key ID** in the **Key Identifier** field. * Upload the `.p8` file in the **Private Key** field. * Paste the **Issuer ID** in the **Issuer Identifier** field. 4. Click **Verify & Fetch Apps**. If everything is correct, Qonversion will show your apps and let you pick the one to configure — Bundle ID and App Store ID will be filled in automatically on the next step. ## 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. ### "No apps are linked to this API key" The credentials are valid, but the API key role doesn't have access to any apps. If you're using **App Manager**, ask your **Admin** to grant the user explicit access to the apps you want to configure. **Admin** keys see all apps automatically. ### Lost the .p8 file Apple does not let you re-download the file. Open **Users and Access → Integrations → App Store Connect API**, **revoke** the old key, then generate a new one and re-upload the new `.p8`. *** ## What's next? After this step, finish the rest of the wizard: * **Step 2 · Core setup** — paste your [App-Specific Shared Secret](ios-app-store-info-fields#2-generate-the-app-specific-shared-secret). * **Step 3 · Server API access** — generate an [In-App Purchase API Key](in-app-purchase-api-key) (separate from this one) and upload it. * **Step 4 · Production readiness** — 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) # 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 your **In-App Purchase API key** — the same key you upload on **Step 3 · Server API access** of the iOS configuration wizard. 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. ### 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 Integrate Apple Search Ads attribution with Qonversion to measure in-app subscription revenue at the ad campaign level. Qonversion automatically collects the attribution data for Apple Ads through its SDK and AdServices Framework. The attribution data and raw data export are available in the Qonversion Apple Ads dashboard. ## 1. Generate Apple Ads credentials 1. Create a user with the API Account Manager role in your Apple Ads [account]() (you need the admin rights to create a user): Account Settings → User Management → Invite Users → Select the user role 2. Log in to your Apple Ads account using the user's credentials with API access created above. You need to create an **OAuth client** in the API settings. To generate private and public keys, please follow [Apple's Documentation](https://developer.apple.com/documentation/apple_search_ads/implementing_oauth_for_the_apple_search_ads_api#3733114). Paste your public key including "BEGIN" and "END" headers to create client credentials. Download the private key, you will need it later: Account Settings → API → Client Credentials ### Create a user with the API Account Manager role If you authorize using the main account, the credentials screen might not always appear. To prevent this issue, create a user with an API access role and use that account to authorize and retrieve the public key. 3. Copy and save `clientId`, `teamId`, `keyId`. 4. Fill **Public Key** with the public part of the certificate that you made in step 2. ## 2. Configure Qonversion with ASA credentials 1. Navigate to the [Apple Ads settings](https://dash.qonversion.io/project/apple-search-ads) in your Qonversion project 2. Provide your **Client ID**, **Team ID**, **Key ID**, **Private key** and save the information. 1. Take **Client ID**, **Team ID**, **Key ID** from the Apple Ads account API settings page. 2. Take **Private Key** from the private part of the certification mentioned above. 1. Do not trim "-----BEGIN EC PRIVATE KEY-----" and "-----END EC PRIVATE KEY-----" from the private key. Paste it in our Settings as is. ## 3. Configure SDK ### 3.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 ### 3.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). ### Done You've successfully set up the Apple Ads attribution with Qonversion. Wait for a day and check the attribution data in Apple Ads dashboard in your Qonversion account. 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 accurately tracks in-app events, including trial conversions, subscription renewals, and in-app purchases, to define top-performing campaigns and keywords. Qonversion also shows you the data on ad spend, CPA, total revenue, and ROAS. The attribution dashboard leverages Apple Ads API and Ad Services framework, SDK data, and server-side receipt validation. Qonversion ASA attribution works independently of the SKAdNetwork. In the Qonversion dashboard navigation this section is still labelled **Apple Search Ads** (route `/apple-search-ads`). Apple's marketing has moved to "Apple Ads", but the dashboard label has not been renamed yet.