# Introduction

devtodev documentation helps you to gain in-depth knowledge of the product and use all opportunities offered by the platform.

**Thank you for choosing devtodev analytics!**&#x20;

{% embed url="<https://youtu.be/gUOtY1bX-qs?feature=shared>" %}

**devtodev** provides SDKs that give you the ability to measure user behavior and player journeys using basic and custom events.&#x20;

We have SDKs for the most popular OS and engines:

<table data-view="cards" data-full-width="false"><thead><tr><th></th><th data-hidden></th><th data-hidden data-card-cover data-type="files"></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td>Android</td><td></td><td><a href="/files/x5jaSMjh2hJ5qSyPXL4e">/files/x5jaSMjh2hJ5qSyPXL4e</a></td><td><a href="/pages/-MhDojIrD6ZBtbz-Umdg">/pages/-MhDojIrD6ZBtbz-Umdg</a></td></tr><tr><td>iOS</td><td></td><td><a href="/files/vjZeXgjJnpf8CyKF51WF">/files/vjZeXgjJnpf8CyKF51WF</a></td><td><a href="/pages/-MhDonSd5-1jauf_EQOw">/pages/-MhDonSd5-1jauf_EQOw</a></td></tr><tr><td>Unity</td><td></td><td><a href="/files/DSZqDvFJrfl2apJ0iFU9">/files/DSZqDvFJrfl2apJ0iFU9</a></td><td><a href="/pages/pM2V35giI3H9bAMJXJ7P">/pages/pM2V35giI3H9bAMJXJ7P</a></td></tr><tr><td>Web</td><td></td><td><a href="/files/bgpopTvEAVoK1fDOgTaJ">/files/bgpopTvEAVoK1fDOgTaJ</a></td><td><a href="/pages/j01RZ6WQtk7lPzICQ3AN">/pages/j01RZ6WQtk7lPzICQ3AN</a></td></tr><tr><td>macOS</td><td></td><td><a href="/files/UrXEVvIrYtSWU91vjTen">/files/UrXEVvIrYtSWU91vjTen</a></td><td><a href="/pages/-MhDoqYkRg--rRDYrA-4">/pages/-MhDoqYkRg--rRDYrA-4</a></td></tr><tr><td>Windows</td><td></td><td><a href="/files/GgBXjppNkdffL51GHtvM">/files/GgBXjppNkdffL51GHtvM</a></td><td><a href="/pages/-MkwFB-F75tzaUO1ocsq">/pages/-MkwFB-F75tzaUO1ocsq</a></td></tr><tr><td>Unreal Engine</td><td></td><td><a href="/files/7q7PQ5lJ4w88F6iwNiWi">/files/7q7PQ5lJ4w88F6iwNiWi</a></td><td><a href="/pages/51zuYZOglW5iBnc9wrN3">/pages/51zuYZOglW5iBnc9wrN3</a></td></tr><tr><td>Godot Engine</td><td></td><td><a href="/files/WgzsuYuA24Dsacbcl7Mo">/files/WgzsuYuA24Dsacbcl7Mo</a></td><td><a href="/pages/p3v8U1Z0FQVNfB9zUuhA">/pages/p3v8U1Z0FQVNfB9zUuhA</a></td></tr></tbody></table>


# Product updates: 2026

## Chart updates in User Flow and Custom events reports

Released: 12/08/2026&#x20;

The User Flow report now supports a Sankey view, where each node represents an event, and each connection represents users moving from one event to the next.&#x20;

Labels are now available in the Custom Events report. The system creates labels for new releases automatically, and you can create your own labels in the Tuning section.&#x20;

[Learn more about User Flow](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/engagement-reports#user-flow)&#x20;

[Learn more about Labels](/reports-and-functionality/project-related-reports-and-fuctionality/tuning#labels)

***

## AI Assistant Chat: build reports in SQL&#x20;

Released: 30/07/2026&#x20;

Now you can ask the Assistant to write an SQL query based on your description. The Assistant will create a report and a link for sharing.&#x20;

{% hint style="success" %}
This feature is in Beta mode. We will greatly appreciate your feedback. Use the [`Contact Us`](https://www.devtodev.com/contact-us) form or reach out to our Customer Success team directly within the platform. Please add **AI ASSISTANT** when submitting your request.
{% endhint %}

[Learn more about AI Assistant](/reports-and-functionality/ai-features#assistant-chat)&#x20;

***

## Dashboard update and alternative events for Custom events report&#x20;

Released: 23/06/2026&#x20;

We've added an option to add tags to **dashboards**. You can now easily organise and sort both reports and dashboards. \
We've also updated our **text widget editor** and added support for images. It can be useful for attaching app screenshots or offer creatives to the corresponding dashboard.&#x20;

The **Custom events** report now supports alternative events. Use it to count different events as one logical event.&#x20;

[Learn more about dashboards](/reports-and-functionality/space-related-reports-and-functionality/dashboards-and-reports#saved-dashboards)&#x20;

[Learn more about Custom Events report](/reports-and-functionality/project-related-reports-and-fuctionality/events-and-funnels#alternative-events)

***

## AI Assistant Chat: more insights

Released: 19/06/2026

The Assistant can now add analytical insights about your report. It can also provide you with information about devtodev features and integration using documentation.&#x20;

{% hint style="success" %}
This feature is in Beta mode. We will greatly appreciate your feedback. Use the [`Contact Us`](https://www.devtodev.com/contact-us) form or reach out to our Customer Success team directly within the platform. Please add **AI ASSISTANT** when submitting your request.
{% endhint %}

[Learn more about AI Assistant](/reports-and-functionality/ai-features#assistant-chat)&#x20;

***

## Copy push campaigns between projects&#x20;

Released: 09/06/2026&#x20;

Previously, you could only duplicate a push notification campaign in one devtodev project. Now you can copy an existing campaign to different projects. This action will help you easily move similar notification content between projects.&#x20;

Please note that when copying to a new project, you need to configure audience settings and media attachments separately. &#x20;

[Learn more about push campaigns](/reports-and-functionality/project-related-reports-and-fuctionality/experiments/push-notifications#copying-an-existing-campaign)

***

## AI Assistant Chat: Report builder

Released: 18/05/2026&#x20;

Simply write your question in the chat, and the Assistant will generate a report to show you the answer. For now, the Assistant can build reports using only the [Basic Metrics](https://docs.devtodev.com/reports-and-functionality/project-related-reports-and-fuctionality/events-and-funnels) report.

{% hint style="success" %}
This feature is in Beta mode. We will greatly appreciate your feedback. Use the [`Contact Us`](https://www.devtodev.com/contact-us) form or reach out to our Customer Success team directly within the platform. Please add **AI ASSISTANT** when submitting your request.
{% endhint %}

[Learn more about AI Assistant](/reports-and-functionality/ai-features#assistant-chat)&#x20;

***

## Fallback proxy support&#x20;

Released: 11/05/2026&#x20;

This feature may be useful when the primary host for devtodev analytics is unreachable from the user's device. The SDK will automatically switch to the next URL in the list if the current host stops responding – for example, if it is blocked by an ISP, a filtering proxy, or an ad-blocking system.

[Learn more about fallback proxy integration](/integration/integration-of-sdk-v2/setting-up-events/secondary-methods#fallback-proxy)

***

## Single Sign-On (SSO)

Released: 19/03/2026

{% hint style="info" %}
Currently available only for the [Enterprise plan](https://www.devtodev.com/pricing).&#x20;
{% endhint %}

Access devtodev securely and seamlessly with your existing identity provider. SSO allows you to log in to devtodev using your corporate credentials, reducing password fatigue and strengthening access control across your organization.&#x20;

{% hint style="success" %}
Reach out to our Customer Success team within the platform or using the [`Contact Us`](https://www.devtodev.com/contact-us) form to learn more and enable SSO for your organization. Please add **SSO** when submitting your request. &#x20;
{% endhint %}

[Learn more about Single Sign-On](/getting-started/registration#single-sign-on-sso)

***

## Session recording for Web projects

Released: 04/02/2026&#x20;

We've added session recording support to our Web SDK. Now you can watch how users interact with your product in a dedicated Session replays report or in the User Card.&#x20;

[Learn more about Session replays](/reports-and-functionality/project-related-reports-and-fuctionality/users/session-replays)

***

## Cross-platform application support&#x20;

Released: 06/01/2026&#x20;

Now you can easily analyse data from your app on different platforms in one devtodev project.&#x20;

Please note that previously created projects cannot be switched to cross-platform. &#x20;

[Learn more about Cross-platform applications](/getting-started/adding-an-app-to-the-space/cross-platform-application)


# 2025

Here are some of the new features you might have missed.

## AI Assistant Summary

Released: 19/12/2025&#x20;

When you open the **Overview dashboard**, our AI assistant will prepare a short summary using data on the dashboard. The AI uses only aggregated, depersonalised data for the summary.

{% hint style="success" %}
This is our first step to AI analytics assistant. Feel free to share your feedback using the [`Contact Us`](https://www.devtodev.com/contact-us) form or reach out to our Customer Success team directly within the platform. Please add **AI SUMMARY** when submitting your request. &#x20;
{% endhint %}

[Learn more about AI Summary](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/overview-dashboard#ai-summary)

***

## Custom event descriptions in Funnels

Released: 28/11/2025&#x20;

Quickly find out the meaning behind an event. In **Conversion funnels** report, hover over an event or a parameter, and a description will appear. If you do not have any event descriptions yet, you can add them manually or generate them using AI in Tuning -> Custom event configurations.&#x20;

[Learn more about Custom event configurations](/reports-and-functionality/project-related-reports-and-fuctionality/tuning#ai-descriptions)

***

## New report: Purchased items&#x20;

Released: 01/10/2025&#x20;

We've updated our **Payment structure** report and added a **Purchased items** tab. Check this report to quickly identify poorly performing items and change your monetization accordingly.&#x20;

If available, this report uses automatically tracked refund data (see [setup requirements](https://docs.devtodev.com/integration/autocapture/automatic-refund-tracking)).&#x20;

[Learn more about Payments structure report](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/monetization-reports#payments-structure)&#x20;

***

## Custom events and Funnels: detailed Tutorial analysis&#x20;

Released: 01/10/2025&#x20;

Before you could check tutorial events only with a **Status** parameter: started, finished or skipped. We've added a **Step** parameter so you can select and analyse the exact steps of your tutorial in Custom events and Funnels reports. This allows for a more detailed FTUE analysis. &#x20;

[Learn more about Custom events and Funnels reports](/reports-and-functionality/project-related-reports-and-fuctionality/events-and-funnels)&#x20;

***

## Basic Metrics: updated Total value overview

Released: 29/09/2025&#x20;

By default, you can see the Total values for selected events above the chart. Previously this field was available only for table views. We've added a `Show total` button so you can hide this information block.&#x20;

Total values are available in Basic Metrics for projects and Space.&#x20;

[Learn more about Basic Metrics report](/reports-and-functionality/project-related-reports-and-fuctionality/events-and-funnels#basic-metrics)

***

## Remote configuration (Beta)

Released: 16/09/2025&#x20;

With the help of remote configuration you can:

1. Change app behavior for all users or just for a specific audience.
2. Conduct A/B tests to compare different configurations on the same audience and find the best-performing one.&#x20;

With the introduction of remote configuration we've also simplified the A/B test integration.&#x20;

{% hint style="success" %}
We will greatly appreciate your feedback. Please add **REMOTE CONFIGS** when submitting your request.
{% endhint %}

[Learn more about remote configuration](/integration/integration-of-sdk-v2/remote-configuration)

***

## New integration: automatic payments from Aghanim

Released: 16/09/2025&#x20;

We've collaborated with Aghanim and added a new way to track payments automatically. After everything is set up, Aghanim will send [Real Payment](/integration/server-api/data-api-2.0#real-currency-payment) events to devtodev via API and we'll match them to users. &#x20;

[Learn more about Aghanim setup](/integration/autocapture/automatic-payment-tracking/aghanim)&#x20;

***

## Custom event descriptions in reports&#x20;

Released: 01/09/2025&#x20;

Quickly find out the meaning behind an event. In Custom events report, hover over an event or a parameter, and a description will appear. If you do not have any event descriptions yet, you can add them manually or generate them using AI in Tuning -> Custom event configurations.&#x20;

[Learn more about Custom event configurations](/reports-and-functionality/project-related-reports-and-fuctionality/tuning#ai-descriptions)

***

## Automatic refunds tracking

Released: 07/08/2025&#x20;

An addition to our [Automatic payments tracking](/integration/autocapture/automatic-payment-tracking) – now you can also get refunds data automatically with a simple setup in devtodev settings. This integration allows you to receive data from App Store and Google Play.&#x20;

[Learn more about automatic refunds tracking](/integration/autocapture/automatic-refund-tracking)

***

## New cohort export integrations: Customer.io, Pushwoosh, Braze&#x20;

Released: 17/07/2025&#x20;

We've added more integrations for our [Cohort export](/reports-and-functionality/project-related-reports-and-fuctionality/tuning#cohort-export) feature. Create segments in devtodev and engage with users using personalised communication.&#x20;

[Learn how to set up Cohort export in devtodev](/3rd-party-sources/cohort-export)

***

## SDK integration with the help of AI&#x20;

Released: 17/06/2025

If you are using AI coding agents, you can try our new AI-assisted integration process. The AI will analyse your app code, suggest the necessary events and help you integrate devtodev SDK quickly.&#x20;

Currently this option is available for Unity projects but we are working on extending the list of platforms.&#x20;

Feel free to send us feedback regarding this feature via [`Contact us`](https://www.devtodev.com/contact-us) form or reach out to our Customer Success team directly within the platform. Please add **AI INTEGRATION** when submitting your request.&#x20;

[Learn more about AI-assisted integration](/integration/integration-of-sdk-v2/sdk-integration/unity/ai-assisted-integration-beta)

***

## Labels on Dashboards&#x20;

Released: 16/05/2025&#x20;

Previously, labels were available only in Basic Metrics reports; now you can see them on the dashboard widgets. The system creates labels for new releases automatically and you can create your own labels in the Tuning section.&#x20;

[Learn more about Labels](/reports-and-functionality/project-related-reports-and-fuctionality/tuning#labels) &#x20;

***

## New integration: send user cohorts to OneSignal&#x20;

Released: 16/05/2025&#x20;

New addition to our [Cohort Export](/reports-and-functionality/project-related-reports-and-fuctionality/tuning#cohort-export) feature – OneSignal. Create a segment in devtodev and use it to send personalised messages via different channels from OneSignal.&#x20;

[Learn how to set up OneSignal in devtodev](/3rd-party-sources/cohort-export#onesignal)

***

## AI-generated descriptions for Custom events

Released: 10/04/2025&#x20;

You can now add descriptions to Custom events. To help you with this process, we've added an option to quickly generate these descriptions using **AI**. You can always edit the generated descriptions manually.&#x20;

We've also updated the **Custom event configurations** page so you can easily check and configure any event and its parameters.&#x20;

[Learn more about Custom event configurations](/reports-and-functionality/project-related-reports-and-fuctionality/tuning#custom-event-configurations)

***

## A/B testing for Web SDK&#x20;

Released: 28/03/2025&#x20;

Our [SDK for Web](/integration/integration-of-sdk-v2/sdk-integration/web/web-sdk-integration) now supports A/B testing. We've also added a [changelog page](/integration/integration-of-sdk-v2/sdk-integration/web/web-sdk-releases) for Web SDK releases.&#x20;

[Learn more about A/B testing integration](/integration/integration-of-sdk-v2/a-b-testing/description-of-a-b-testing-on-the-sdk-side)&#x20;

***

## Automatic payment tracking

Released: 26/03/2025&#x20;

Devtodev already tracks [Subscriptions](/basic-events-and-custom-events#subscriptions) automatically. We've added automatic tracking for In-App Purchases. Now you can receive data about transactions from App Store and Google Play without integrating the [Real Payment event](/basic-events-and-custom-events#real-payment).&#x20;

This allows you to speed up analytics integration and get valid data directly from the store.&#x20;

[Learn more about automatic payment tracking](/integration/autocapture/automatic-payment-tracking)


# 2024

Here are some of the new features you might have missed.

## Create Alerts in Basic Metrics reports

Released: 21/11/2024&#x20;

It is now possible to create an alert for a number of metrics straight from the report. Click on the `Create alert` button in the three dots menu and you will be promted to the Alerts wizard with the preselected metric.&#x20;

[Learn more about the Basic Metrics report](/reports-and-functionality/project-related-reports-and-fuctionality/events-and-funnels#create-alerts)

***

## Custom Events: updated Total value overview

Released: 21/11/2024&#x20;

By default you can see the Total values for selected events above the report. We've added a `Show total` button so now you can hide this information block. We've also moved all the report customization settings (Metrics settings and Conditional Formatting) to the left for easier navigation.&#x20;

[Check out this article for more details](https://www.devtodev.com/promo/news/10459/total-value-in-the-custom-events-report)

***

## Widgets: last update time

Released: 21/11/2024

You can now check if the data in the widget is up to date and update it without the need to refresh the whole dashboard. Click on three dots menu and select `Refresh data`.&#x20;

[Check out this article for more details](https://www.devtodev.com/promo/news/10457/widget-update-data-timestamp)

***

## Report sharing update&#x20;

Released: 17/10/2024&#x20;

We have added a **Select All** option so you can easily share your report or dashboard with everyone. If you want to share multiple reports, there is now an option to batch select and share them in the **Saved reports** or **Saved Dashboards** section.&#x20;

[Check out this article for more details](https://www.devtodev.com/promo/news/10455/simplified-dashboard-sharing)

***

## Dashboard control management update&#x20;

Released: 13/08/2024&#x20;

**Unified Control for Report Filters and SQL Variables:** Before this update you could only add a Period type control for widgets based on non-SQL reports (Basic metrics / Custom Events / Funnel reports). Now it is possible to create and apply common filters such as Country, Language, Channel, Campaign and Paying status.&#x20;

**Simplified default variable value change:** You can change the default value directly in the `Manage controls` panel, without the need to navigate to the original widget.&#x20;

[Learn more about dashboard controls](/reports-and-functionality/space-related-reports-and-functionality/sql#change-widgets-variable-values-on-a-dashboard)

***

## Add Player Levels report to custom dashboards

Released: 13/08/2024&#x20;

The Player Levels report has some unique ready-to-use metrics such as Gross revenue and number or players remaining at a specific level. You can now add this report as a widget to a custom dashboard and get the full picture for your game analysis.&#x20;

[Learn more about the Player Levels report](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/in-game-analysis-reports#player-levels)

***

## Export widgets to CSV&#x20;

Released: 22/07/2024&#x20;

Save the report results from the dashboard without opening the original report using Export to CSV. The result will be saved as table. This can be useful for further data processing or sharing the report to colleagues without access to devtodev.

[Learn more about dashboard widgets](/reports-and-functionality/space-related-reports-and-functionality/dashboards-and-reports#export-widgets-to-csv)

***

## Add Conversion to payments report to custom dashboards

Released: 21/06/2024&#x20;

You can now save Conversion to N payment, Period until payments, Top converting goods charts to a custom dashboard as widgets. This information will help you analyse the payments in a more convenient way.&#x20;

[Learn more about the Conversion to payments report](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/monetization-reports#conversion-to-payments)&#x20;

***

## Like operator in Custom Events, Funnels and User Flow

Released: 21/06/2024&#x20;

The **Like** opertor allows you to create a mask for the string parameter values.&#x20;

For example, you have a parameter called *Item* and it has values: *offer1*, *offer2*, *offer3*, *offer4.* Use the **Like** operator and type *offer*, this will select all four of these values in the resulting report.

[Learn more about Custom events and Funnels reports](/reports-and-functionality/project-related-reports-and-fuctionality/events-and-funnels)&#x20;

[Learn more about the User Flow report](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/engagement-reports#user-flow)

***

## Add Locations report to custom dashboards

Released: 21/06/2024&#x20;

The Locations report contains ready-made metrics that allow you to see how users pass the different locations. You can now save this report to a dashboard with other metrics to see the whole picture.

[Learn more about the Location report](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/in-game-analysis-reports#locations)

***

## Change metrics' names in Custom events report&#x20;

Released: 14/05/2024&#x20;

This update gives you an ability to change the name of any metric in Custom events. You can use more convenient titles and adjust the values with a Round setting. There is also an option to change the Units for a single number widget. The applied metrics' settings will also be saved when you share the report or add it to a dashboard.&#x20;

[Learn more about the Custom events report](/reports-and-functionality/project-related-reports-and-fuctionality/events-and-funnels#custom-events)

***

## Add Cumulative ARPU report to custom dashboards&#x20;

Released: 14/05/2024&#x20;

Results from Devtodev’s Cumulative ARPU report can now be saved to a custom dashboard as a widget. This information helps you track crucial Cumulative ARPU metric values for your project.

[Learn more about the Cumulative ARPU report](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/monetization-reports#cumulative-arpu)

***

## Acquisition: trafic source filter

Released: 30/04/2024

Previously data from Custom Postback API or SDK Install referrer was not available in the Acquisition section. Now, we have added a **Source** filter in the Detailed stats report, where you can select the needed trafic sources.&#x20;

[Learn more about the Detailed stats report](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/acquisition-reports#detailed-stats)

***

## Table widget settings

Released: 30/04/2024&#x20;

[Customize the tables](/reports-and-functionality/space-related-reports-and-functionality/dashboards-and-reports#table-widget-settings) on your dashboard. Improve readability by adjusting the column width automatically (**Autofit column width**) or by hand. We have also added a **Wrap text** option for heading and table rows to allow for longer titles.

[Check out this article for more details](https://www.devtodev.com/promo/news/10444/customizing-tables-on-dashboards)

***

## Extended period of segment expiration

Released: 10/04/2024&#x20;

With this update, we have extended the expiration date for unused segments to 60 days. If you do not apply the segment to any report, it will expire. However, you will have 30 days to recover this segment before it’s completely deleted.&#x20;

[Learn more about Segments](/reports-and-functionality/project-related-reports-and-fuctionality/users#segments)

***

## Axis scale settings

Released: 21/03/2024&#x20;

Users now are able to **customize axis boundaries** on their charts to more accurately represent data trends. In cases where the charts are not optimally positioned (or you are not happy with their placement, or want to change the position of the chart), simply clicking on the Axis Scale option allows you to define the minimum and maximum values for the axis.&#x20;

[Check out this article for more details](https://www.devtodev.com/promo/news/10442/update-axes-scale-settings)

***

## Managing content created by deleted users

Released: 20/03/2024&#x20;

When a user deletes their account, or in case the user is removed from the space, their content remains in devtodev. Now, the team can manage the ownership of such content. For example, you can take ownership of the report made by the deleted user, or you can delete it from the space.

[Learn more about User management](/space-management#managing-content-from-deleted-users)

***

## Transfer space ownership to another user&#x20;

Released: 20/03/2024

When the space owner is changing, they need to transfer their ownership. With this update, the owner of the space can select a new owner in User settings and delegate their owner access.&#x20;

[Learn more about ownership transfer](/space-management#transfer-ownership-to-another-user)

***

## Cohort Timespent in Basic Metrics

Released: 12/03/2024

We’ve added a new metric to our Basic Metrics report – the Cohort Timespent. Previously, you may have known this metric as “Cohort Playtime,” which was created specifically to evaluate user engagement in the Sessions report. And now this metric is also available in Basic Metrics for both game projects and applications.&#x20;

[Check out this article about Cohort Timespent](https://www.devtodev.com/promo/news/10440/cohort-timespent-in-basic-metrics)

***

## devtodev SDK for Godot Engine

Released: 27/02/2024

Now you can create games and apps from scratch on all popular engines, including Godot, all while utilizing detailed data analytics tools. Devtodev platform will support you in understanding player behavior, optimization, and increasing user engagement throughout the entire lifetime of the product.&#x20;

The new Godot SDK is now available for iOS, macOS, and Android platforms, providing you with the tools you need to take your games and apps to the next level.&#x20;

[Learn how to integrate Godot SDK](/integration/integration-of-sdk-v2/sdk-integration/godot-engine)

***

## Copy dynamic segments

Released: 28/01/2024&#x20;

An existing **dynamic segment** can be duplicated with just one click. Simply copy the segment and adjust its settings to save time when creating a segment with similar conditions.

[Learn more about Segments](/reports-and-functionality/project-related-reports-and-fuctionality/users#copying-a-segment)

***

## More Alert options

Released: 09/01/2024

You can receive notifications if the **number of basic or custom events** suddenly changes or differs from a specific day. These alerts will inform you about any positive or negative fluctuations.

Also, **two new conditions** have become available: a comparison with the previous day and a comparison with the same day last week. This allows the alert to activate when yesterday's event count is different from the count on the chosen day.

[Learn more about Alerts](/reports-and-functionality/project-related-reports-and-fuctionality/tuning#alerts)


# 2023

Here are some of the new features you might have missed.

## Add Retention report to custom dashboards&#x20;

Released: 20/12/2023

Results from Devtodev’s Retention report can now be added to a custom dashboard as a widget. This information helps you track crucial retention metric values for your project. The update eliminates the need for SQL in building the report and adding it to the dashboard.&#x20;

Here are some examples of how you can use this functionality:

* Add a widget with data on the retention of users who signed up for a specific subscription plan.&#x20;
* Display the retention of users who signed up for your project and then performed a desired action (e.g., created a project, started a workout, or invited a friend).&#x20;
* Compare the retention of users who engaged with your new features with those who did not (e.g., completed/skipped onboarding).&#x20;
* Add several widgets to compare the retention of users who used different methods of doing something (e.g., project creation, battle modes, price plans, payment methods, etc.).

[Learn more about Retention reports](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/engagement-reports#retention)

***

## IS NOT operator and new parameter options in reports

Released: 05/12/2023

Devtodev’s Custom event, Custom funnels, and User flow reports got a new operator and parameter value that will increase their flexibility. Use the 'IS NOT' operator and 'null' parameter value to exclude certain parameters and to work with events that do not return any value.&#x20;

[Check out this article for more details](https://www.devtodev.com/promo/news/10434/new-operator-and-parameter-options-in-reports)&#x20;

***

## Manage User properties in one click

Released: 21/11/2023&#x20;

You can disable any property that you have previously added and now **consider unnecessary or faulty**. For example, if you initially collected the 'user type' property (such as beginner or pro), but have since decided to focus on the 'user status' property (guest/registered), you can disable the 'user type' property to maintain a cleaner and more organized report-building process.&#x20;

[Learn how to manage User Properties](/reports-and-functionality/project-related-reports-and-fuctionality/tuning#user-property)

***

## 1x1 widget in Custom events

Released: 20/11/2023&#x20;

With this update a familiar 1x1 widget from SQL and Basic Metrics becomes available in the Custom events report. Select Report type -> Number and a single number for your most important KPI is ready for your dashboard.&#x20;

Here are some examples of what you can check: number of in-app shop openings, average battle time in a game, conversion from workout start to finish.

[Learn more about Custom events report](/reports-and-functionality/project-related-reports-and-fuctionality/events-and-funnels#custom-events)

***

## App type selection

Released: 30/10/2023&#x20;

When creating a new devtodev project, you can now select the type of project: a game or an application. This choice will affect the customization of the devtodev interface. Depending on the type, specialized reports will be available. &#x20;

You can always change the type of the project later in Settings -> General settings.&#x20;

[Learn more about adding new projects](/getting-started/adding-an-app-to-the-space)

***

## Convenient 1x1 widget from Basic Metrics report

Released: 19/09/2023

Devtodev users are familiar with the 1x1 widget — a small module on your dashboard containing **a single number designed to keep you informed about important metrics**. With the latest update from Devtodev, you no longer need to use SQL to create widgets with basic metrics. You can simply open the Basic Metrics report, select a metric, click View -> Number, and then add this number as a widget to your dashboard. This will not only save you valuable time but also enable you to use a limited number of SQL widgets to display other metrics.&#x20;

[Check out this article for more details](https://www.devtodev.com/promo/news/10431/convenient-1x1-widget-from-basic-metrics-report)

***

## 1x1 Widget upgrade

Released: 29/08/2023&#x20;

When it comes to data visualization, Devtodev offers plenty of powerful tools, and SQL widgets is one of them. To give you access to even more data points on your dashboards, we’ve introduced an update that offers you the ability to display **two metrics on a single 1x1 widget**. Simply write a single query and get two numbers: it is easy, convenient and requires less time than creating separate queries for two individual widgets.&#x20;

[Check out this article for more details](https://www.devtodev.com/promo/news/10430/1x1-widget-upgrade)

***

## Simplified Funnel views

Released: 16/08/2023&#x20;

Devtodev’s clients frequently create funnels to analyze various aspects of their apps and games. However, these **funnels are often longer** than a single screen, and the users find themselves having to scroll excessively, which can be quite inconvenient.

To alleviate this issue, we have recently introduced the 'Step' option to the [*Conversion funnel* ](https://docs.devtodev.com/reports-and-functionality/project-related-reports-and-fuctionality/events-and-funnels#conversion-funnels)report. It is designed to save our clients time and effort while streamlining the funnel exploration process.&#x20;

[Check out this article for more details](https://www.devtodev.com/promo/news/10429/update-simplified-funnel-views)

***

## Space Overview update

Released: 25/07/2023&#x20;

The overview provides essential metrics for all projects in Space, allowing users to conventionally track the metrics of the projects. Additionally, it offers the option to access the prepared reports in the selected project to analyze metric rises or falls. The overview aims to briefly introduce each project's key metrics and keep users informed about devtodev's news.

[Learn more about Space overview](/reports-and-functionality/space-related-reports-and-functionality/space-dashboard)

***

## Customize Tutorial steps

Released: 18/07/2023

This update empowers you to enhance collaboration among team members by giving each step in the [Tutorial analysis](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/engagement-reports#tutorial-analysis) report a meaningful and easy-to-understand name of your choice.

By tapping on the step in the report, you can assign a **name that describes the action** and facilitate seamless collaboration among all team members working on the app.

[Check out this article for more details](https://www.devtodev.com/promo/news/10428/devtodev-update-customize-tutorial-steps)

***

## Basic metrics report: percentage share

Released: 27/06/2023

In our latest update we added **distribution by parameter** (Show % of total) across all the Basic metrics report tables. This feature proves particularly valuable when you need to determine the **percentage of users with a specific attribute** in relation to the total number of users in a particular group. It’s extremely convenient because you don’t need to waste time on creating separate SQL queries anymore!&#x20;

[Check out this article for more details](https://www.devtodev.com/promo/news/10427/basic-metrics-report-percentage-share)

***

## Enhanced Retention report

Released: 20/06/2023

devtodev’s [Retention reports](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/engagement-reports#retention) has got several new features that will enable you to **narrow down the audiences** and calculate this metric more effectively.&#x20;

With this update, we’ve introduced two additional options for two calculation methods (by calendar days and by 24-hour interval):

* **Flexible calculation start**: you can now choose a specific event or several events that define a cohort for retention calculation, allowing for more tailored analysis..
* **Custom calculation end**: you have the freedom to select a particular event that signifies a user’s return to the product, enabling more precise measurement.&#x20;

[Check out this article for more details](https://www.devtodev.com/promo/news/10426/devtodev-update-enhanced-retention-report)

***

## Updated Funnels: more events and Churned users segment

Released: 25/05/2023

This update got you **two new features** in the *Funnel report* *(Reports -> Conversion funnel)*: an alternative event that you can use when building a funnel, and an option for creating a segment of users who failed to complete all the funnel steps.&#x20;

[Check out this article for more details](https://www.devtodev.com/promo/news/10425/updated-funnels-more-events-and-churned-users-segment)

***

## Updated Virtual Goods & Purchases report&#x20;

Released: 02/05/2023&#x20;

This time we added several great features to the report’s [Virtual goods and purchases](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/in-game-analysis-reports#virtual-goods-and-purchases) section. They allow for a much deeper analysis of in-game currency, purchases, and real money spends.&#x20;

What we did:

* *Added display of item groups*. The groups may come in handy in case you have too many items.&#x20;
* *Added breakdown by levels*.&#x20;
* Introduced a number of purchases divided by the DAU metric value (“*% of active*”, as in the Custom events report).&#x20;
* *Improved mean value.* After the update, we calculate it as the number of items purchased at a level divided by the number of users.

[Check out this article for more details](https://www.devtodev.com/promo/news/10424/updated-virtual-goods-and-purchases-report)

***

## Data labels in reports

Released: 17/04/2023&#x20;

You can now activate Data labels on charts in Basic Metrics, Custom events or SQL reports. Use them to evaluate the dynamics and values in one glimpse.&#x20;

[Learn more about reports](/reports-and-functionality/project-related-reports-and-fuctionality/events-and-funnels#basic-metrics)

***

## Export Dashboards to PDF

Released: 04/04/2023

Sharing dashboards has never been so easy and convenient! devtodev already has a wide range of tools that make your work more smooth and unhindered, however, the **export to pdf tool** takes the process to the next level.

It’s an extremely convenient option for both senders and recipients. The **senders** can make sure that the recipients get only the data that they want to share with them in the exact form that they intended to show them. The **recipients** can get access to data without going through the whole process of signing up to the devtodev platform.

[Check out this article for more details](https://www.devtodev.com/promo/news/10423/easy-dashboard-sharing-with-the-export-to-pdf-option)

***

## Funnels: limit by session, rename steps

Released: 07/03/2023

Analyzing [FTUE](https://www.devtodev.com/education/articles/en/348/main-metrics-ftue) using devtodev **funnels** is as easy as it can be! To make the process more convenient, we are introducing ‘**limitation by session**’ — a new additional option that will help you with analyzing any particular user session. Now building a funnel is as easy as creating  any devtodev out-of-the-box reports — simply click and analyze every aspect of it.

If you are curious about the number of users who achieve the ultimate goal **during the first or the Nth session** (first two, three, etc.), you can easily calculate them, build a segment and then analyze thoroughly. Simply click on ‘Conversion time limit’, set a sequential number of the session and build a funnel.

Now you can also rename funnel steps!&#x20;

[Check out this article for more details](https://www.devtodev.com/promo/news/10422/-use-session-limits-to-build-smart-funnels)

***

## SDK Update: full complience with COPPA

Released: 16/02/2023&#x20;

If you have a child-directed app or game, you may worry that the data you send to devtodev is anonymised and protected enough. It’s not a problem anymore with our updated SDK because it operates in full compliance with COPPA — the **Children's Online Privacy Protection Act**.&#x20;

This updated version of devtodev SDK does not collect, process or store ad IDs of children. It simply **creates anonymised user identification numbers** that can be used by the platform for tracking underaged users without influencing the decisions they make.&#x20;

If you already use devtodev SDK, all you need to do is to **enable the COPPA-compliance opinion** before SDK initialization and delete the dependencies necessary for processing ad and vendor IDs. This will not interfere with your working process because almost **all devtodev reports will stay available** and you will be able to use them as you did before.

Read more about it in our documentation: [iOS](/integration/integration-of-sdk-v2/sdk-integration/ios#apps-targeted-at-children), [Android](/integration/integration-of-sdk-v2/sdk-integration/android#apps-targeted-at-children), and [Unity](/integration/integration-of-sdk-v2/sdk-integration/unity#apps-targeted-at-children)

***

## New metric in A/B tests: CARPU

Released: 14/02/2023&#x20;

We’ve added 1, 7, 14 and 30-day Cumulative ARPU to the A/B tests. You can use these metrics to improve the quality of the analysis and prove that the implemented changes did not influence other key metrics of the app.&#x20;

[Learn more about A/B testing](/integration/integration-of-sdk-v2/a-b-testing/working-with-a-b-tests-in-the-devtodev)

***

## User Flow: more session analysis options

Released: 17/01/2023

Drill down into your user flow data by **setting a limit on the number of user sessions**! This option comes in handy in case you want to take a closer look at what your users managed to achieve during, let’s say, their first session, and compare it with your expectations.&#x20;

There are **three options** in this report - “No”, “Limit by first session”, and “Limit by number of sessions”. If you want to see events that users completed during their first session, choose the second option. If you want to see data for several sessions, choose the third option. In this case, the calculation is as follows: we take the specified number of sessions performed during the selected period, then we take the dates of the first and the last sessions, and after that we analyze the events performed during this period.

[Check out this article for more details](https://www.devtodev.com/promo/news/10420/updated-user-flow-report-more-session-analysis-options)


# Getting Started

Here are some simple steps to start using our service:

1. [Sign up for devtodev](/getting-started/registration)
2. [Create your space   ](/getting-started/adding-a-space)
3. [Add a project to the space   ](/getting-started/adding-an-app-to-the-space)
4. [Integrate SDK to your project](/integration/integration-of-sdk-v2/sdk-integration)
5. [Integrate basic devtodev events](/integration/integration-of-sdk-v2/setting-up-events/basic-methods)
6. [Integrate custom events   ](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#custom-events)
7. [Check your integration   ](/integration/expert-tips/check-your-integration)
8. [Add anti-cheat methods   ](/integration/integration-of-sdk-v2/setting-up-events/anticheat-methods)
9. [Mark Test devices](/integration/test-devices)
10. [Add App Store/Google Play account](/3rd-party-sources/app-marketplace-data)


# Registration

## Sign up

1. Visit our website [www.devtodev.com](<https://www.devtodev.com >) and click `Get started`.&#x20;

<figure><img src="/files/B5dZSvl9LyofX2Pd3rWs" alt=""><figcaption></figcaption></figure>

2. Fill out the registration form.&#x20;

<figure><img src="/files/EA9Xj3OCyOAQhLWyMISS" alt=""><figcaption></figcaption></figure>

3. You will receive an email to confirm your registration.&#x20;

<figure><img src="/files/PIVhriLaaUVVf6b79gen" alt=""><figcaption></figcaption></figure>

## Single Sign-On (SSO)

{% hint style="info" %}
SSO is available only for the [Enterprise plan](https://www.devtodev.com/pricing).
{% endhint %}

**Single Sign-On** allows you to log in to devtodev using your corporate credentials, reducing password fatigue and strengthening access control across your organization.&#x20;

SSO can be configured with your organization’s identity provider. Once activated, team members authenticate via your company’s secure login system – no separate devtodev password required.&#x20;

{% hint style="success" %}
Reach out to our Customer Success team within the platform or using the [`Contact Us`](https://www.devtodev.com/contact-us) form to learn more and enable SSO for your organization. Please add **SSO** when submitting your request. &#x20;
{% endhint %}


# Adding a space

Space is an information field where you will work. Later on, you will add your application to the space.

1. Click `Create Space`.&#x20;

<figure><img src="/files/Gb9Jtbp48A1msOi2a42s" alt=""><figcaption></figcaption></figure>

2. To create a space, you need to fill in:

* Name
* Timezone
* Logo (optional)

The timezone is important because it defines the time when one day ends and another one begins.&#x20;

<figure><img src="/files/UJuHKoL2gpSrxybdNZiY" alt=""><figcaption></figcaption></figure>

3. Fill in your billing information.&#x20;

That's it! You've created your space, and now you can add applications to it.

We offer a 30-day free trial for all new users (only for the first space created using this email).&#x20;

{% hint style="info" %}
**Note that the trial period begins at the moment you create a Space.** &#x20;
{% endhint %}

<figure><img src="/files/02hQsvsd1jgpVZ3UfF51" alt=""><figcaption></figcaption></figure>


# Adding an app to the space

## Add Application

Once you've created the space, you'll see the `Add Application` button in the devtodev interface. Click on it to add an application to the space.

<figure><img src="/files/6Ssb0oFzwUdfTpAp9UQK" alt=""><figcaption></figcaption></figure>

## Standalone vs. Cross-platform: choosing the right project

When setting up a new project in devtodev, one of the first and most consequential decisions you'll make is choosing between a **standalone** and a **cross-platform** project type.&#x20;

{% hint style="warning" %}
**This choice is permanent – you cannot switch a previously created devtodev project to the cross-platform type – so it's worth getting right from the start.**&#x20;
{% endhint %}

It is also impossible to send data from one application to both types of projects simultaneously.

### What's the Difference?

{% columns %}
{% column %}
A **standalone project** tracks a single platform (e.g., iOS, Android, or PC). Each platform gets its own devtodev project with its own user base, metrics, and reports.

**Best for:** subscription apps, apps that use A/B testing or push campaigns heavily, or any app where platform-specific reporting is a priority.
{% endcolumn %}

{% column %}
A **cross-platform project** is designed for apps that share a user base across multiple operating systems. It combines user data from multiple platforms under a single project, giving you a unified view of your audience.&#x20;

**Best for:** productivity or utility apps available on multiple OSes, or any product where understanding the full cross-device user lifecycle is more valuable.&#x20;
{% endcolumn %}
{% endcolumns %}

### When to choose Standalone&#x20;

A standalone project is the right fit when:

* **Your app targets a single platform.** A standalone project is simpler and fully featured.
* **Most of your users are interacting with only one platform (device)** and you want to analyze them independently without cross-contamination of metrics.
* **You use Push Notifications, A/B tests, or Remote Configuration.** If these are part of your growth toolkit, select standalone.&#x20;
* **You track subscriptions.** Subscription tracking and the Subscriptions report are available only for standalone projects.&#x20;
* **You rely on store integrations.** Standalone projects support market data integrations with Google Play, the Apple App Store, and others. Cross-platform projects do not support store integrations for market data.&#x20;
* **You need cohort exports.** Cohort export is available only in standalone projects.

### When to choose Cross-platform

A cross-platform project makes sense when:

* **Most of your users run the app on different devices with the same account frequently.** You can see the history of users and metrics across all platforms in one place
* **You want a unified user identity across platforms.** Cross-platform projects give users the same devtodev ID on all platforms (in devtodev). This lets you track the full journey of a single user regardless of what device they're on.&#x20;
* **You can manage the technical prerequisite.** Cross-platform projects require you to use your own custom identifiers (User ID).&#x20;
* **You need cross-platform behavioral insights.** The User Card shows all events a user performed on different platforms, and most reports include a platform filter so you can drill into individual platforms when needed.&#x20;

### Quick comparison

<table><thead><tr><th width="400.703125">Feature</th><th align="center">Standalone</th><th align="center">Cross-platform</th></tr></thead><tbody><tr><td>Multiple platforms in one project</td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="274c">❌</span></td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="2705">✅</span></td></tr><tr><td>Unified user identity across platforms</td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="274c">❌</span></td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="2705">✅</span></td></tr><tr><td>Push Notifications</td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="2705">✅</span></td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="274c">❌</span></td></tr><tr><td>A/B Tests &#x26; Remote Configuration</td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="2705">✅</span></td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="274c">❌</span></td></tr><tr><td>Store integrations (Google Play, App Store)</td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="2705">✅</span></td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="274c">❌</span></td></tr><tr><td>Subscription tracking</td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="2705">✅</span></td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="274c">❌</span></td></tr><tr><td>Cohort export</td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="2705">✅</span></td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="274c">❌</span></td></tr><tr><td>Platform filter in reports</td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="274c">❌</span></td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="2705">✅</span></td></tr><tr><td>Custom User ID required</td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="274c">❌</span></td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="2705">✅</span></td></tr></tbody></table>

***

{% columns %}
{% column %}
If you would like to add a **Standalone application**, check the following page:&#x20;

{% content-ref url="/pages/6M2Hi7fMPXega45ffu0W" %}
[Standalone application](/getting-started/adding-an-app-to-the-space/standalone-application)
{% endcontent-ref %}

{% endcolumn %}

{% column %}
If you would like to add a **Cross-platform application**, check the following page:&#x20;

{% content-ref url="/pages/EBjsmNHf2V14SSZuYNZr" %}
[Cross-platform application](/getting-started/adding-an-app-to-the-space/cross-platform-application)
{% endcontent-ref %}

{% endcolumn %}
{% endcolumns %}


# Standalone application

## Adding a standalone application

1. First, you need to select a platform.

<figure><img src="/files/UkByMypc8nAeEiqEoBfN" alt=""><figcaption></figcaption></figure>

2. The next step is optional. You can test the integration in test mode. It assumes that up to 100 different users can use the app, and their data will be excluded from statistics. \
   When you [switch the test mode off](/reports-and-functionality/project-related-reports-and-fuctionality/settings#switch-to-production-mode), all the users who had sent the data will be marked as [testers](/integration/test-devices) in your project.&#x20;

<figure><img src="/files/e48SDUS14N1QBcShIjNX" alt=""><figcaption></figcaption></figure>

3. During the next step, you can add an account to collect data from the application store or skip this step and add this information later in project [Settings](/reports-and-functionality/project-related-reports-and-fuctionality/settings#app-marketplace-stats).&#x20;

<figure><img src="/files/uRgBAtNB8z4FMJ69LgFg" alt=""><figcaption></figcaption></figure>

4. The final step is to fill in the name of your app and select its genre and type.\
   Select app type: game or app. \
   If you choose “app” as the type, gaming events will not be tracked and displayed in the interface, even if they are integrated. Also, game-related elements will be hidden in the interface.

You can change the project type at any time in the Settings → [General settings](/reports-and-functionality/project-related-reports-and-fuctionality/settings#general-settings) section.&#x20;

<figure><img src="/files/rjR58bItCKJhq11pYxf5" alt=""><figcaption></figcaption></figure>

Please note that you can select more than one category.&#x20;

<figure><img src="/files/T7cgds8MUAKz1Suede47" alt=""><figcaption></figcaption></figure>

Congratulations, you have added the application to the space!&#x20;

## Next steps

Now you can see the standard devtodev interface and all the reports. Of course, the reports are empty until you start [sending data](/basic-events-and-custom-events) to devtodev.

If you have added an account to collect data from the application store, it will take 1 day to build the report.

If your integration uses SDK (in most cases), the next step is to integrate SDK into your app. Please read our [expert tips on integration](/integration/expert-tips), select the necessary [SDK](/integration/integration-of-sdk-v2), [integrate](/integration/integration-of-sdk-v2/setting-up-events/basic-methods) it, and [start using devtodev's full functionality](/reports-and-functionality/project-related-reports-and-fuctionality/events-and-funnels)!&#x20;

And please don't hesitate to ask us questions. You can find the `Contact us` button in the top right of the devtodev interface.&#x20;

<figure><img src="/files/1PR7Ed5LA8DG0E1Nlb3H" alt=""><figcaption></figcaption></figure>


# Cross-platform application

Cross-platform is a new type of devtodev project for applications designed to run on different operating systems using a shared codebase. The project combines user data from multiple platforms.

{% hint style="info" %}
**Please note that you cannot switch previously created devtodev projects to a cross-platform type!**
{% endhint %}

{% hint style="warning" %}
**Prerequisite**

Cross-platform projects use identification by User ID by default. You need to use your own custom identifiers and set them using the [setUserID](/integration/integration-of-sdk-v2/setting-up-events/user-profile#user-id) method during SDK initialization.&#x20;

**Required SDK version**: 2.6.0 (Unity 3.10.0) and higher.
{% endhint %}

## Limitations

For now, cross-platform projects **do not support**:

* Push Notifications, A/B tests or Remote Configuration;
* Store integrations for market data (Google Play, Apple App Store etc.);
* Cohort export;&#x20;
* Subscription tracking and Subscriptions report.&#x20;

{% hint style="info" %}
One project can contain up to five platforms.&#x20;
{% endhint %}

## Adding a cross-platform application&#x20;

1. Select **Cross-platform application** as your app type and click `Next`.&#x20;

<figure><img src="/files/jbbaucdVTC1Q45bbHvZE" alt=""><figcaption></figcaption></figure>

2. Give your project a name and select an app type. \
   If you choose the “app” type, gaming events will not be tracked and displayed in the interface, even if they are integrated. Game-related reports will be hidden in the interface. \
   \
   Optionally, you can enable a [Test mode](/integration/test-devices) to exclude data received during integration. \
   \
   Click `Next` to proceed.

<figure><img src="/files/FMiVReMKWFcS9SL0DMzJ" alt=""><figcaption></figcaption></figure>

3. Click `Finish` to add the project ot devtodev. Next, you will need to add at least one platform. You will be redirected to Settings to complete the process.&#x20;

<figure><img src="/files/SlM85TdH76RtbCiYiUg9" alt=""><figcaption></figcaption></figure>

### Adding a platform

{% hint style="info" %}
One project can contain up to five platforms.&#x20;
{% endhint %}

1. Click `Add Platfrom` to select a platform for your app.&#x20;

<figure><img src="/files/2neAEF5wYKLjFr9tryeT" alt=""><figcaption></figcaption></figure>

2. Select a platform type from the drop-down list and add a name. This name will appear in the report filters and in project settings.&#x20;

{% hint style="info" %}
Note: You will not be able to change the platform after creation, however, you will be able to delete it.&#x20;
{% endhint %}

Click `Save` to finish.&#x20;

<figure><img src="/files/ku6YsSf5WTNCD4NOK9Ok" alt=""><figcaption></figcaption></figure>

### Edit platform&#x20;

Click on the `pencil icon` to make changes. &#x20;

<figure><img src="/files/eRsQVd4o9gV7x2ibGz7T" alt=""><figcaption></figcaption></figure>

You can change the name of the platform.&#x20;

It is possible to delete a platform when there is more than one platform in the list. Click `Delete` and confirm the action.&#x20;

{% hint style="warning" %}
When you delete a platform, devtodev stops receiving events from this platform and hides historical data from the reports. \
**If you would like to save the historical data, do not delete the platform – remove the `Platform ID` from your integration code.** &#x20;
{% endhint %}

<figure><img src="/files/206oT7t8uexpU0rMohay" alt=""><figcaption></figcaption></figure>

## Integration

The integration process is similar to a standalone type of application. The only difference is an additional Platform ID.&#x20;

* [**SDK**](/integration/integration-of-sdk-v2/sdk-integration): simply copy the `App ID + Platform ID` in the initialization code of the corresponding platform app.
* [**Data API**](/integration/server-api/data-api-2.0): you will need to send the `Platfrom Id` separately with every package as a `platform` field.&#x20;

<figure><img src="/files/ctMB8d26FUhGflNt2MZm" alt=""><figcaption></figcaption></figure>

## Revenue rate settings

You can set up revenue rate and transaction check rules for different platforms in Settings -> Payments integration -> [Payments settings](/reports-and-functionality/project-related-reports-and-fuctionality/settings#payments-settings).

## Cross-platform features

### Platform filter&#x20;

In most of the reports you will see a **Platforms** filter at the top of the list. By default, the reports will show data from all platforms. You can select a specific platform to inspect it in more detail.

<figure><img src="/files/SZ3rkqITWpll7riyHobZ" alt=""><figcaption></figcaption></figure>

Some reports, like Transactions, will allow you to select several platforms at once.&#x20;

<figure><img src="/files/f6lHT13lu15lf0z5yI1G" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
If the report does not have a platform filter, it will use the data from all platforms at once.&#x20;
{% endhint %}

### User card&#x20;

Here you can check all events the user performed on different platforms.&#x20;

The user has the same devtodev ID on all platforms.&#x20;

In the Basic properties tab you will see a dedicated list of properties for each platform and general information about the user in the General section. For example, *Install date* and device information are platform-specific, so they will be different for each platform section.&#x20;

The General section stores the *Install date* of the first-ever platform and updates the *Last seen* field according to the latest data from any platform.&#x20;

<figure><img src="/files/g8dgAQPed0qZqQZxyLdc" alt=""><figcaption></figcaption></figure>

Cheater/Tester marks are connected to the User card and are **not** platform-specific. If you mark a user for a cheat transaction on one platform, the user becomes a cheater on **all platforms** and their transactions become invalid.&#x20;

### SQL&#x20;

Each table has a `platform` parameter, the value corresponds to `Platform ID` in project Settings.&#x20;

The cross-platform projects store users from all platforms in corresponding tables and also in a common `users` table. You can find a platform-specific users table by Platform ID in the name postfix.&#x20;

<figure><img src="/files/sPHjTHJCcu0vBJhE0s4M" alt=""><figcaption></figcaption></figure>


# Basic Events & Custom Events

To begin working with analytics, you need to start sending events (information about user activity in the project) to the analytics system.

devtodev provides basic events that drive the majority of the reports.

## Sessions

Users start with opening the app, and this process we call "starting a session". For your convenience, in the majority of devtodev SDKs this event is integrated automatically.

{% hint style="warning" %}
There is no separate event in devtodev that describes a session by its duration and start or end date.
{% endhint %}

We use two events that we can use to examine the duration of the sessions. The first event captures the start date of a new session and the second captures the duration of each application activity (time when application is in focus). In our experience, this is the most reliable solution at the moment.

At the moment when application receives focus (this may be the moment the SDK is initialized / the application is opened / the device wakes up from the sleep mode with the focus on the application), we begin to assume that the application becomes active and begin to count the duration of the activity.&#x20;

If the application loses focus (minimizing the application / device is going into the sleep mode / switching to another application / exiting the application), we consider that the activity is completed and we send its duration to the devtodev server. This is an application activity event.

We consider the start of a new session to be the moment when the application receives focus (see the description above), but at the same time we take into account that more than 10 minutes have passed since the last activity of the application. Otherwise, we count that the previously launched session continues. \
More detailed information on tracking sessions can be found on the [Track Sessions](/integration/integration-of-sdk-v2/setting-up-events/track-sessions) page.

{% hint style="info" %}
To calculate the **average session length per day**, the sum of all durations of application activity is divided by the number of session starts.
{% endhint %}

#### Reports based on a Session event&#xD;:

* [Engagement Dashboard](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/engagement-reports#engagement-dashboard)
* [Sessions](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/engagement-reports#sessions)
* [Retention](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/engagement-reports#retention)
* [Audience structure](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/engagement-reports#audience-structure)
* [Churn Rate](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/engagement-reports#churn-rate)
* [App Version Analysis](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/engagement-reports#app-versions-analysis)

## Real Payment

If users can use real money to make purchases in your project, you need to integrate the Payment event. When devtodev receives data about sessions and payments in your app, we can calculate all financial metrics (gross, revenue, average check) and metrics of the project’s financial efficiency (ARPU, ARPPU, paying share, LTV).

[How to integrate Payment event](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#real-payment)

{% hint style="info" %}
Integration of data about sessions and payments takes up to 20% of the time spent on integration, but in the future it will give answers to 80% of questions about user behavior in the project.
{% endhint %}

#### Reports based on a Payment event&#xD;:

* [Monetization Dashboard](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/monetization-reports#monetization-dashboard)
* [Gross Structure](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/monetization-reports#gross-structure)
* [Conversion to Payments](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/monetization-reports#conversion-to-payments)
* [Cumulative ARPU](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/monetization-reports#cumulative-arpu)
* [RFM Analysis](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/monetization-reports#rfm-analysis)
* [Payments amounts](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/monetization-reports#payment-amounts)
* [Transactions](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/monetization-reports#transactions)

## **Subscriptions**

If your application has subscriptions, you can integrate this event to analyze the financial data as well as the structure of your subscribers. Devtodev integrations allow you to receive information about subscription purchases even if the user doesn’t open the app (auto-renewable subscriptions).

[How to integrate the Subscriptions event (SDK)](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#subscriptions)&#x20;

[How to set up Subscriptions integration with Store](/3rd-party-sources/app-marketplace-data)&#x20;

#### Reports based on Subscription events:

* [Monetization Dashboard](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/monetization-reports#monetization-dashboard)&#x20;
* [Subscriptions](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/monetization-reports#subscriptions)&#x20;
* [Gross Structure](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/monetization-reports#gross-structure)&#x20;
* [Conversion to Payments](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/monetization-reports#conversion-to-payments) &#x20;
* [Cumulative ARPU](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/monetization-reports#cumulative-arpu)&#x20;
* [RFM Analysis](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/monetization-reports#rfm-analysis)&#x20;
* [Payments amounts](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/monetization-reports#payment-amounts)&#x20;
* [Transactions](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/monetization-reports#transactions)&#x20;

## **Onboarding (tutorial)**&#x20;

{% hint style="info" %}
Tutorial is a very important part of any project because the first session lays the foundation for future retention and monetization indicators.
{% endhint %}

Using this event you can evaluate the effectiveness of the tutorial steps system, analyze how users complete the tutorial, find bottlenecks, and measure the time it takes for users to complete the tutorial. The event should be sent at the end of each tutorial step indicating the number of completed steps as a parameter.&#x20;

Here are some predefined values for the Tutorial Step event parameter:

* &#x20;0 - the user skipped the tutorial
* -1 - the user started the tutorial
* -2 - the user finished the tutorial

[How to integrate Tutorial Step event](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#onboarding-tutorial)

#### Reports based on a Tutorial Step event:

* [Tutorial Analysis](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/engagement-reports#tutorial-analysis)

## Custom Events

devtodev is a universal analytics system, and our structure of basic events is designed for game projects specifically. However, there may be situations when your project requires events that are not provided by the devtodev basic events. Such events can still be sent and analyzed in our system - as custom events.

{% hint style="info" %}
Each custom event can have up to 20 parameters (see [Limits](/data-management-and-limits#data-limits)) to identify more detailed info about users' behavior.&#x20;
{% endhint %}

Here are some examples of users' actions that can be sent as custom events: when users open an in-game store, click on items, buy items. Based on these events, you can build a funnel and see the conversion on each step.

**Some limits for custom events:**

1. The number of different event types sent from one project shouldn't exceed 300 (see [Limits](/data-management-and-limits#data-limits)).
2. The event name must not exceed 72 characters.
3. One event can contain up to 20 parameters, which must have unique names of up to 32 symbols.
4. Parameters can be string and numeric:\
   \- the maximum length of parameter values is 255 characters;\
   \- the number of string parameter values cannot exceed 50 000; when it exceeds 50 000, the further sending of information about the parameter and possibilities to work with it will be blocked.

**Here is some expert advice to avoid problems with limits in custom events:**

* There is no need to send user IDs in parameters (they are collected by default).
* Do not send time in the timestamp format (it is also collected by default).

[How to integrate Custom events](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#custom-events)

#### Reports based on Custom events:

* [Conversion Funnels](/reports-and-functionality/project-related-reports-and-fuctionality/events-and-funnels#conversion-funnels)
* [User Flow](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/engagement-reports#user-flow)
* [Custom Events](/reports-and-functionality/project-related-reports-and-fuctionality/events-and-funnels#custom-events)

## LevelUp

This event is for games only. It is worthwhile to integrate this event into a game type project, as specified in the [application settings](/getting-started/adding-an-app-to-the-space#add-application). In projects with the “app” type, game events will not be tracked and displayed in the interface, even if they are integrated. You can verify and change the project type in Settings → [General settings](/reports-and-functionality/project-related-reports-and-fuctionality/settings#general-settings).

You can analyze the distribution of the players over the levels. Many game projects have levels, which means that as users become more experienced, they gradually increase their level. In this case levels have a linear structure: the level N is followed by the level N+1.\
If your project has in-game currency, using the LevelUp event you can send information about the current amount of in-game currency players have. This data allows evaluating the average amount of in-game currency that players have on a particular level.

The event should be sent right after the player reaches the next level.&#x20;

[How to integrate LevelUp event](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#level-up)

#### Reports based on a LevelUp event:

* [In-game Analysis Dashboard](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/in-game-analysis-reports#in-game-analysis-dashboard)
* [Economy Balance](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/in-game-analysis-reports#economy-balance)
* [Virtual Goods & Purchases](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/in-game-analysis-reports#virtual-goods-and-purchases)
* [Game Structure (Player levels tab)](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/in-game-analysis-reports#game-structure)

### CurrencyAccrual

This event is for games only. It is worthwhile to integrate this event into a game type project, as specified in the [application settings](/getting-started/adding-an-app-to-the-space#add-application). In projects with the “app” type, game events will not be tracked and displayed in the interface, even if they are integrated. You can verify and change the project type in Settings → [General settings](/reports-and-functionality/project-related-reports-and-fuctionality/settings#general-settings).

This specific event is a part of the LevelUp event and does not require a separate dispatch. Send this event to track the average amount of in-game currency earned or purchased during a level after each time an in-game account is replenished.

[How to integrate CurrencyAccrual event](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#currency-accrual)

#### Reports based on a Currency Accrual event:

* [In-game Analysis Dashboard](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/in-game-analysis-reports#in-game-analysis-dashboard)
* [Economy Balance](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/in-game-analysis-reports#economy-balance)

## Progression Event

This event is for games only. It is worthwhile to integrate this event into a game type project, as specified in the [application settings](/getting-started/adding-an-app-to-the-space#add-application). In projects with the “app” type, game events will not be tracked and displayed in the interface, even if they are integrated. You can verify and change the project type in Settings → [General settings](/reports-and-functionality/project-related-reports-and-fuctionality/settings#general-settings).

There are many projects (for example, Match-3 games), where players make an attempt to complete a level. Their attempts may be either successful or unsuccessful. In addition, during a certain attempt different numerical indicators can be changed: the number of stars, resources, in-game currency.

To analyze such attempts, devtodev provides a basic Progression event. In the Progression event you send information about how players pass a particular game location, whether their attempt is successful, and how numerical indicators change.

{% hint style="info" %}
The sequence in which locations are passed is not important in this case: after the location N players can go to any location M.
{% endhint %}

[How to integrate Progression Event](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#progression-event)

#### Report based on a Progression event:

* [Game Structure (Locations tab)](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/in-game-analysis-reports#game-structure)&#x20;

## Virtual Currency Payment

This event is for games only. It is worthwhile to integrate this event into a game type project, as specified in the [application settings](/getting-started/adding-an-app-to-the-space#add-application). In projects with the “app” type, game events will not be tracked and displayed in the interface, even if they are integrated. You can verify and change the project type in Settings → [General settings](/reports-and-functionality/project-related-reports-and-fuctionality/settings#general-settings).

Many games, especially f2p, have in-game currency. Players can spend it on virtual goods. To work with virtual currency purchases, devtodev has developed the Virtual Currency Payment event.

[How to integrate Virtual Currency Payment Event](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#virtual-currency-payment)

#### Reports based on a Virtual Currency Payment events:

* [In-game Analysis Dashboard](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/in-game-analysis-reports#in-game-analysis-dashboard)
* [Economy Balance](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/in-game-analysis-reports#economy-balance)
* [Virtual Goods & Purchases](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/in-game-analysis-reports#virtual-goods-and-purchases)


# Integration


# Expert Tips


# What to track

You can find full description of devtodev events in this article:&#x20;

{% content-ref url="/pages/-Lo4NftfOJ\_6eQSHNbtK" %}
[Basic Events & Custom Events](/basic-events-and-custom-events)
{% endcontent-ref %}

## **Where to start**

[**Tutorial** ](/basic-events-and-custom-events#onboarding-tutorial)is a very important part of any project because the first session lays the foundation for future retention and monetization indicators.

devtodev allows to analyze how users complete the tutorial, find bottlenecks, and measure the time it takes for users to complete the tutorial. These questions can be answered with the help of the [Tutorial analysis report](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/engagement-reports#tutorial-analysis).&#x20;

<figure><img src="/files/0GwO9KFFcab48a7C2UfF" alt=""><figcaption></figcaption></figure>

## **If users in your project become more experienced and raise their level**

Many game projects have levels, which means that as users become more experienced, they gradually increase their level. In this case levels have a linear structure: the level N is followed by the level N+1.

When players move to the next level, you need to use the [**LevelUp event**](/basic-events-and-custom-events#levelup). Reports such as Economy balance and Player levels are built by levels and are based on this event.&#x20;

Also, if your project has in-game currency, you can send information about the current amount of in-game currency players have using the **LevelUp** event. This data allows to evaluate the average amount of in-game currency that players have on a particular level.

For example, this is the [Player levels report](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/in-game-analysis-reports#player-levels). It shows how users are distributed among levels, the percentage of users who remain on a particular level, the revenue of a particular level, etc.&#x20;

<figure><img src="/files/mGFndAbcY8I8CQEF32PF" alt=""><figcaption></figcaption></figure>

## **If it is possible to pass / fail a level in your game**

There are many projects (for example, Match-3 games), where players attempt to pass a level. Their attempts may be either successful or unsuccessful. In addition, during a certain attempt some numerical indicators can change: the number of stars, resources, in-game currency.

To analyze these attempts, we've created a basic [**Progression event**](/basic-events-and-custom-events#progression-event). With Progression event you can send information about how players pass a particular game location, whether their attempt was successful, and how numerical indicators change.

Based on the **Progression event**, we build the [Locations report](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/in-game-analysis-reports#locations), where all indicators are calculated by game locations, for successful and unsuccessful attempts.&#x20;

<figure><img src="/files/XxoZxODTb5GVzbuDqUCi" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
The sequence of locations passing is not important in this case: after the location N, players can go to any location M.
{% endhint %}

## **If there is a virtual currency in your project**

Many games, especially f2p, have in-game currency. Players can accumulate currency, or buy it for real money. They can then spend it on virtual goods. To work with virtual currency, devtodev has developed the following events:

* [**Virtual Currency Payment**](/basic-events-and-custom-events#virtual-currency-payment) – to send information about purchases made by players. Please note that these are only purchases made with virtual currency, while information about purchases for real money is sent with the [Payment event](/basic-events-and-custom-events#real-payment).
* [**Currency Accrual**](/basic-events-and-custom-events#currencyaccrual) – to show information about movements of virtual currency. For example, if a player earns currency or receives it for some actions, you can use Currency Accrual to see this information.

All the game economy reports are based on these events.

With the help of the **Currency Balances by Level** tab in the [Economy Balance report](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/in-game-analysis-reports#economy-balance) (this one also requires a [LevelUp event](/basic-events-and-custom-events#levelup)), you can see how users spend, earn and accumulate currency on each game level.&#x20;

<figure><img src="/files/LHlX882zCo5mZUUqtVQs" alt=""><figcaption></figcaption></figure>

The **Top Purchases** tab in the [Virtual goods & Purchases report](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/in-game-analysis-reports#virtual-goods-and-purchases) allows you to analyze the structure of the consumer basket and identify the most popular items among different categories of players.&#x20;

<figure><img src="/files/aCxzd5EQk2vd89pi5VrG" alt=""><figcaption></figcaption></figure>

## Custom Events

There may be situations when your project requires events that cannot be tracked by devtodev basic events. Such events can still be sent and analyzed in our system as [**custom events**](/basic-events-and-custom-events#custom-events). It is possible to specify parameter values of custom events.&#x20;

Here are some examples of user actions that can be sent as custom events: opening an in-game store, clicking on items, buying items. Based on these events, you can then build a funnel and see the conversion on each step.

**Some limits for custom events:**

1. The number of different event types sent from one project should not exceed 300. &#x20;
2. The event name must not exceed 72 characters.
3. One event can contain up to 20 parameters, each of them with unique names of up to 32 symbols.
4. Parameters can be string or numeric:&#x20;
   1. The maximum length of string parameter values is 255 characters.
   2. The number of unique string parameter values cannot exceed 50000. When it exceeds 50000, the system will block the parameter.&#x20;

**Here is some expert advice to avoid problems with limits on custom events:**

* There is no need to send user IDs in parameters (they are collected by default).
* Do not send time in the timestamp format (it is also collected by default).

## **If you want to check transactions for validity**

To exclude from statistics transactions made by cheaters, you can use devtodev [**Anticheat method**](/integration/integration-of-sdk-v2/setting-up-events/anticheat-methods). By using this method, you will be able to check payments for validity before sending them to devtodev.&#x20;

The verification process is the following:

1. Get response about a completed transaction from the payment system.
2. Either send data about the received transaction for verification by calling devtodev anti-cheat methods or use your own tools for transaction verification.
3. If the transaction has successfully passed verification, perform the Payment event.   &#x20;\
   If the transaction has not passed verification, do not perform the Payment event.

{% hint style="warning" %}
We do not recommend to use devtodev Anticheat method as the only tool to validate transactions.
{% endhint %}


# Payments & Anti-cheat

Payment event integration and using anti-cheat methods

Here you'll find the principles of processing data about real payments, tips for the integration of the Payment event and anti-cheat methods used in devtodev.

## Payment event integration

Gross metrics are one of the key indicators of the app’s performance. Therefore, it is important to approach the integration of the [Payment event](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#real-payment) very seriously.

There are four parameters that are sent in the Payment event:

1. Transaction identifier
2. Item name
3. Item price in payment currency
4. Payment currency identifier.

Let’s look at each of the parameters and things to keep in mind when specifying their values while integrating devtodev SDK.

### Transaction identifier&#x20;

This is one of the transaction parameters where invalid values occur most often.&#x20;

Here are the requirements for this parameter:

1. The transaction identifier is a string value of max 64 symbols. In case this limit is exceeded the value will be shortened to 64 symbols.&#x20;
2. The identifier must be unique. Data about the transaction with already registered identifier will be discarded by the system and will not be included in statistics.
3. We recommend using the identifier that has been assigned to the transaction by the payment system as the transaction identifier.&#x20;
4. In case your app is designed for Apple (iPhone, iPad, iPhone+iPad, or Mac) or Android (Google Play) platforms, the use of the transaction identifier assigned by the app store is mandatory!&#x20;
   1. Transaction identifiers that come from apps on these platforms are checked by devtodev for their compliance with the format used by these markets. This allows us to discard the most obvious cheat transactions.&#x20;
   2. It is also important to know that users who made these transactions are marked as cheaters and all their subsequent transactions are not included in statistics (you can disable this verification process in the [Settings](/reports-and-functionality/project-related-reports-and-fuctionality/settings#transaction-checking-terms)).

### Item name

The item name is a string value that should not exceed 255 symbols. One of the most common mistakes when specifying the value of this parameter is specifying the localized name of the item in multi-language apps. This leads to the appearance of many records that describe the same item in different reports (for example, [Virtual goods & purchases](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/in-game-analysis-reports#virtual-goods-and-purchases)).&#x20;

One way to avoid this situation is to specify the name of the item bundle as its name.

### Item price and currency identifier&#x20;

The item price parameter contains the sum that a user paid for the item in a payment currency. The price is specified as a floating-point number.&#x20;

The currency identifier parameter must specify the currency as a three-letter code according to ISO 4217 standard (examples: USD, EUR, JPY, CNY).

When the Payment event reaches devtodev servers, before transaction data is saved, the sum is automatically converted to USD at the actual currency rate at that moment.

{% hint style="warning" %}
In case the currency identifier is not specified or the identifier is invalid, the transaction is considered invalid and is not counted in statistics.&#x20;
{% endhint %}

If after converting to USD the sum exceeds $1500, the transaction is considered invalid and is not counted in statistics as well (this verification can be disabled in the [Settings](/reports-and-functionality/project-related-reports-and-fuctionality/settings#transaction-checking-terms)).\
When the transaction is made with an in-game currency of social network, you first need to convert this currency to any real-world currency.

It is also important to remember that the sum of the purchase sent in the Payment event shows the actual sum that the user paid (this data is used to built Gross metrics).&#x20;

In order to see your net income (Revenue metrics), you need to specify the [revenue rates](/reports-and-functionality/project-related-reports-and-fuctionality/settings#revenue-rate) to calculate net profit within your total revenue. The rate can be specified as single or individual for each country. This increases the accuracy in case the part of the sum is spent on taxes and fees that are individual for each country.

## Payment validation

Unfortunately, in some situations filling in the parameters of the Payment event is not enough for getting valid data in reports, since there can be cheat transactions. There are several ways to deal with this problem, but all of them are based either on preliminary verification of the transaction or detection of suspicious user actions.

To prevent cheat transactions from getting into the report, you need to check the transaction in advance and omit sending the Payment event If the transaction turns out to be invalid.&#x20;

Or you can mark the user/device as a cheater and exclude their further data from all reports. It is possible to combine both methods for greater reliability.&#x20;

The process of detecting cheaters based on their behavior within apps depends on the specificity of a particular app. If you have implemented such an algorithm, you can mark suspicious users as cheaters to avoid getting data on their payments in reports. \
To do that, you just need to execute an SDK [method ](/integration/integration-of-sdk-v2/setting-up-events/user-profile#cheater)or mark users via [API](/integration/server-api/data-api-2.0#people).

One of the conditions for increasing the reliability of transaction verification is implementing it outside of the client app. You can create the system of verification and place it on your own servers or use our out-of-the-box solution – devtodev anti-cheat system.

{% content-ref url="/pages/-MhDshyfvOboO0jcGGte" %}
[Anticheat methods](/integration/integration-of-sdk-v2/setting-up-events/anticheat-methods)
{% endcontent-ref %}

devtodev anti-cheat allows to check the validity of transactions from the following app stores:

* Apple App Store
* Google Play Store
* Microsoft Store (UWP).

## Recommended sequence of actions when working with transactions&#x20;

1. Get response about a completed transaction from the payment system.
2. Either send data about the received transaction for verification by calling devtodev anti-cheat methods or use your own tools for transaction verification.
3. If the transaction has successfully passed verification, perform the Payment event. \
   If the transaction has not passed verification, do not perform the Payment event.

{% hint style="warning" %}
We do not recommend to use the result of devtodev anti-cheat verification as a condition for giving or not giving in-game currency or item purchased by user.&#x20;
{% endhint %}


# Check your integration

## Test devices&#x20;

Add a device to test your integration:

{% content-ref url="/pages/-LyhC09fh8UWY-lEkA-B" %}
[Test Devices](/integration/test-devices)
{% endcontent-ref %}

## Apps targeted at children&#x20;

When developing and publishing apps targeted at children under 13 years old, you need to ensure special conditions for data processing.&#x20;

Check out how to enable compliance mode for [Android](/integration/integration-of-sdk-v2/sdk-integration/android#apps-targeted-at-children), [iOS](/integration/integration-of-sdk-v2/sdk-integration/ios#apps-targeted-at-children), [MacOS](/integration/integration-of-sdk-v2/sdk-integration/macos#apps-targeted-at-children) and [Unity](/integration/integration-of-sdk-v2/sdk-integration/unity#apps-targeted-at-children).

## How to check event logs in devtodev interface&#x20;

You can check the incoming events in the User card ([Users & Segments section](/reports-and-functionality/project-related-reports-and-fuctionality/users#users)) or in the [Event Log](/reports-and-functionality/project-related-reports-and-fuctionality/settings#event-log) (Settings -> SDK -> Integration -> Event Log).&#x20;

To check events faster, mark the User card as a Tester. This way the log in the User card will not be cached and the events will appear much quicker.&#x20;

## Configure Revenue rates and transaction checks

You can change the default revenue rates for your project in Settings -> SDK -> [Payments](/reports-and-functionality/project-related-reports-and-fuctionality/settings#payments-settings).&#x20;

If necessary, you can also disable some of the [transation checks](/reports-and-functionality/project-related-reports-and-fuctionality/settings#transaction-checking-terms) and set up transaction value limits.

## How to enable SDK debug logs

Change `LogLevel` value to `DTDLogLevel.Debug` in the Initialization configuration.&#x20;

Check out examples for different platforms in the [Integration](/integration/integration-of-sdk-v2/sdk-integration) section.

## Data collected by devtodev automatically&#x20;

Some of the events and properties are sent to devtodev automatically by the SDK.&#x20;

### Information about a device/user:&#x20;

* Event timestamp&#x20;
* Timezone&#x20;
* Tracking status (iOS, Android) – the state of the flag for permission of ad tracking.
* App version (must be specified by developer for WEB projects).&#x20;
* devtodev SDK version&#x20;
* Push token – in the case of an additional initialization by the developer and with a user’s permission.
* Device manufacturer (iOS, Android)&#x20;
* Device model (iOS, Android)
* Language – device’s locale data.
* OS type
* OS version&#x20;
* Rooted/Jailbreaked OS flag&#x20;
* User agent string&#x20;
* Different device IDs depending on the platform. Disabled when [COPPA Control](#apps-targeted-at-children) is enabled.

### Automatically sent events:

* **Session start** – beginning of application activity with the date.
* **Activity period** – duration of application activity.
* The **source** of app install (only from Google Play), sent once.

### Data received by the server from queries’ metadata&#x20;

* **Install date** – date of the first launch of an application with integrated devtodev SDK.
* **Last seen date** – date of the last incoming query.
* **IP-address** – anonymized IP-address.
* **Country** – defined by IP.


# Integration of SDK 2.0+


# SDK Integration


# Android

The SDK is available as an AAR (recommended) and JAR library. The library is available in the MavenCentral and [GitHub repository](https://github.com/devtodev-analytics/android-sdk-2.0).

## Google implementation

{% hint style="warning" %}
**Attention!**&#x20;

From the SDK version `com.devtodev:android-analytics:'2.2.3'` and above you need to add `com.devtodev:android-google:'1.0.1'`.&#x20;

This framework encapsulates work with Google ads ID. When developing and publishing apps for kids [COPPA](https://www.ftc.gov/tips-advice/business-center/privacy-and-security/children%27s-privacy), you don’t need `com.devtodev:android-google`. You can find more information about working with [COPPA](https://www.ftc.gov/tips-advice/business-center/privacy-and-security/children%27s-privacy) at the end of this guide.
{% endhint %}

### Step 1. **Declare repositories**

In the Project **`build.gradle`**  file, declare the `mavenCentral` repository:

```groovy
repositories {
   //.. other repositories 
   mavenCentral()
}
```

### Step 2. Add Gradle Build Dependencies

If you use Gradle for building apps specify the following dependencies in the application **`build.gradle`** file.&#x20;

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
dependencies {
    // Requirement
    implementation ("com.google.code.gson:gson:*.*.*")
    implementation ("com.google.android.gms:play-services-ads-identifier:*.*.*")
    // Starting from version 2.2.3 and above, it is required
    implementation ("com.devtodev:android-google:*.*.*")

    // if you use AAR (recommended) or JAR downloaded from GitHub, please add:
    implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))

    // or just add the dependency, get the latest version from
    // https://mvnrepository.com/artifact/com.devtodev/android-analytics
    implementation ("com.devtodev:android-analytics:*.*.*")

    // Optional (recommended)
    implementation ("com.android.installreferrer:installreferrer:*.*.*")
}
```

{% endtab %}

{% tab title="Groovy" %}

```groovy
dependencies {
    // Requirement
    implementation 'com.google.code.gson:gson:*.*.*'
    implementation 'com.google.android.gms:play-services-ads-identifier:*.*.*'
    // Starting from version 2.2.3 and above, it is required
    implementation 'com.devtodev:android-google:*.*.*'
    
    // if you use AAR (recommended) or JAR downloaded from GitHub, please add:
    implementation fileTree(dir: "libs", include: ["*.aar"]) 
    
    // or just add the dependency, get the latest version from
    // https://mvnrepository.com/artifact/com.devtodev/android-analytics
    implementation 'com.devtodev:android-analytics:*.*.*'
    
    // Optional (recommended)
    implementation 'com.android.installreferrer:installreferrer:*.*.*'
}
```

{% endtab %}
{% endtabs %}

#### Peculiarities of working with dependencies <a href="#peculiarities-of-working-with-dependencies" id="peculiarities-of-working-with-dependencies"></a>

**Working with Advertising ID with Android API level less than 26**

If you plan to use `com.google.android.gms:play-services-ads-identifier:18.2.0` and above, you need to add `com.android.tools:desugar_jdk_libs` to maintain backward compatibility with devices with API level less than 26, see [![](https://www.gstatic.com/devrel-devsite/prod/vec94db9b1329e6c4d1d9b6b24ba16ad6c02043dcd66ba9c6a8f3d8fa0af3eec7/android/images/favicon.svg)Use Java 8 language features and APIs  |  Android Studio  |  Android Developers](https://developer.android.com/studio/write/java8-support)

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
android {
    defaultConfig {
        // Required when setting minSdkVersion to 20 or lower
        multiDexEnabled = true
    }
    compileOptions {
        // Flag to enable support for the new language APIs
        coreLibraryDesugaringEnabled = true
        // Sets Java compatibility to Java 8
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}
dependencies {
    // For AGP 7.4+
    coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:2.0.3")
    // For AGP 7.3
    // coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:1.2.3")
    // For AGP 4.0 to 7.2
    // coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:1.1.9")
}
```

{% endtab %}

{% tab title="Groovy" %}

```groovy
android {
    defaultConfig {
        // Required when setting minSdkVersion to 20 or lower
        multiDexEnabled true
    }
    compileOptions {
        // Flag to enable support for the new language APIs
        coreLibraryDesugaringEnabled true
        // Sets Java compatibility to Java 8
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}
dependencies {
    // For AGP 7.4+
    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
    // For AGP 7.3
    // coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.3'
    // For AGP 4.0 to 7.2
    // coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.9'
}
```

{% endtab %}
{% endtabs %}

We also recommend AGP 8.0+ as it makes it easier to configure `com.android.tools:desugar_jdk_libs`

## Huawei implementation

### Step 1. **Declare repositories**

If you use Gradle for compiling apps, declare the following dependencies in the build.gradle file in the dependency block:

```groovy
repositories {
   //.. other repositories 
   mavenCentral()
   maven { url 'https://developer.huawei.com/repo/' }
}

allprojects {
    repositories {
        //.. other repositories
        mavenCentral()
        // Configure the Maven repository address for the HMS Core SDK.
        maven {url 'https://developer.huawei.com/repo/'}
    }
}
```

### Step 2. Add Gradle Build Dependencies

In the Project `build.gradle` file declare the agconnect plugin

```groovy
dependencies {
    classpath 'com.huawei.agconnect:agcp:*.*.*'
}
```

In the application build.gradle file declare the following dependencies:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
dependencies {
    // Requirement
    implementation ("com.google.code.gson:gson:*.*.*")
    implementation ("com.huawei.agconnect:agconnect-core:*.*.*")
    implementation ("com.huawei.hms:opendevice:*.*.*")
    // Starting from version 2.2.3 and above, it is required
    implementation ("com.devtodev.android-huawei:*.*.*")
    implementation ("com.huawei.hms:ads-identifier:*.*.*")
    
    // if you use AAR (recommended) or JAR downloaded from GitHub, please add:
    implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
    
    // or just add the dependency, get the latest version from
    // https://mvnrepository.com/artifact/com.devtodev/android-analytics
    implementation ("com.devtodev:android-analytics:*.*.*")
}
```

{% endtab %}

{% tab title="Groovy" %}

```groovy
dependencies {
    // Requirement
    implementation 'com.google.code.gson:gson:*.*.*'
    implementation 'com.huawei.agconnect:agconnect-core:*.*.*'
    implementation 'com.huawei.hms:ads-identifier:*.*.*'
    implementation 'com.huawei.hms:opendevice:*.*.*'
    // Starting from version 2.2.3 and above, it is required
    implementation 'com.devtodev.android-huawei:*.*.*'
    
    // if you use AAR (recommended) or JAR downloaded from GitHub, please add:
    implementation fileTree(dir: "libs", include: ["*.aar"]) 
    
    // or just add the dependency, get the latest version from
    // https://mvnrepository.com/artifact/com.devtodev/android-analytics
    implementation 'com.devtodev:android-analytics:*.*.*'
}
```

{% endtab %}
{% endtabs %}

And add a plugin:

```groovy
plugins {
    //.. other plugins
    id 'com.huawei.agconnect'
}
```

For more information see [huawei official documents](https://developer.huawei.com/consumer/en/doc/development/hiai-Guides/config-maven-0000001050040031).

### Step 3. AppGallery

The `com.devtodev.android-huawei` framework works with[ OAID](https://developer.huawei.com/consumer/en/doc/development/HMS-Plugin-Guides/oaid-0000001050316244) and [ODID](https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/odid-0000001051063255) IDs. In case the [OAID](https://developer.huawei.com/consumer/en/doc/development/HMS-Plugin-Guides/oaid-0000001050316244) is undefined, we use the [ODID](https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/odid-0000001051063255). For both IDs to work correctly, take the following steps:&#x20;

1. Create a project and an app in AppGallery. Open AppGalleryConnect → Project Settings.
2. Sign your app using a certificate (see [here](https://developer.android.com/studio/publish/app-signing)).
3. Enter SHA-256 certificate in the App information section. Read more about certificate creation [here](https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/config-agc-0000001050166285#EN-US_TOPIC_0000001054452903__section10260203515546).

After taking all the steps described above, open the ‘App information’ section and download agconnect-services.json. You need to place this file in the app folder ([read more](https://developer.huawei.com/consumer/en/doc/development/hiai-Guides/add-appgallery-0000001050038080)).

{% hint style="warning" %}
If during testing the app you see that [OAID](https://developer.huawei.com/consumer/en/doc/development/HMS-Plugin-Guides/oaid-0000001050316244) is unavailable and [OAID](https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/odid-0000001051063255) throws [errors](https://developer.huawei.com/consumer/en/doc/development/HMS-2-References/hmssdk_huaweiiap_api_reference_errorcode), you need to first of all check that the tested build is signed with a certificate.
{% endhint %}

## SDK Initialization

Use the following way to initialize the library in the first Activity **`onCreate()`** method:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
      
        val analyticsConfiguration = DTDAnalyticsConfiguration()
        analyticsConfiguration.logLevel = DTDLogLevel.Error
        DTDAnalytics.initialize("App ID", analyticsConfiguration, context = this)
    }
}
```

{% endtab %}

{% tab title="Java" %}

```java
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        DTDAnalyticsConfiguration configuration = new DTDAnalyticsConfiguration();
        configuration.setLogLevel(DTDLogLevel.Error);
        DTDAnalytics.INSTANCE.initialize("App ID", configuration, context);
    }
}
```

{% endtab %}
{% endtabs %}

You can find the `App ID` in the settings of the respective app in devtodev (Settings → SDK → Integration → [Credentials](/reports-and-functionality/project-related-reports-and-fuctionality/settings#integration)).&#x20;

For [Cross-platform type projects](/getting-started/adding-an-app-to-the-space/cross-platform-application) use `App ID + Platform ID` .

* **`config`** - is a **`DTDAnalyticsConfiguration`** object instance that is used for specifying additional properties during initialization.

**`DTDAnalyticsConfiguration`**

<table data-header-hidden><thead><tr><th width="257.43845371312307">Parameter</th><th width="181">Type</th><th>Description</th></tr></thead><tbody><tr><td>Parameter</td><td>Type</td><td>Description</td></tr><tr><td><strong><code>currentLevel</code></strong></td><td>Integer</td><td>The player level at the moment of devtodev SDK initialization. It’s optional but we recommend using it for improving data accuracy.</td></tr><tr><td><strong><code>userId</code></strong></td><td>String</td><td>A custom user ID assigned by the developer. In the case of default calculation by device IDs, the identifier can be used for searching users in devtodev. In case the project uses calculation by user IDs, the parameter is mandatory because it becomes the principal calculation ID in devtodev.</td></tr><tr><td><strong><code>trackingAvailability</code></strong></td><td>DTDTrackingStatus (enum)</td><td>The property allows or disallows devtodev tracking of the user. By default, it is set to <em><strong><code>DTDTrackingStatus.enable</code></strong></em>. SDK stores the previously assigned value. Pass <em><strong><code>DTDTrackingStatus.disable</code></strong></em> if the user opted out of tracking in line with GDPR.</td></tr><tr><td><strong><code>logLevel</code></strong></td><td>DTDLogLevel (enum)</td><td>The level of logging the SDK activity. The <em><strong><code>DTDLogLevel.no</code></strong></em> value is used by default. For troubleshooting during integration it is recommended to set it to <em><strong><code>DTDLogLevel.debug</code></strong></em>, and either switch it off <em><strong><code>DTDLogLevel.no</code></strong></em>. Use <em><strong><code>DTDLogLevel.no</code></strong></em> in the release version.</td></tr></tbody></table>

Example:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val config = DTDAnalyticsConfiguration()
config.currentLevel = 1
config.userId = "CustomUserID"
config.trackingAvailability = DTDTrackingStatus.Enable
config.logLevel = DTDLogLevel.No
DTDAnalytics.initialize("App ID", config, context = this)
```

{% endtab %}

{% tab title="Java" %}

```java
DTDAnalyticsConfiguration config = new DTDAnalyticsConfiguration();
config.setCurrentLevel(1);
config.setUserId("CustomUserID");
config.setTrackingAvailability(DTDTrackingStatus.Enable);
config.setLogLevel(DTDLogLevel.No);
DTDAnalytics.INSTANCE.initialize("App ID", config, context);
```

{% endtab %}
{% endtabs %}

## **SDK obfuscation rules**

Add the following strings to the **`proguard-rules.pro`** file of your app

```kotlin
-keep class com.devtodev.** { *; }
-dontwarn com.devtodev.**
// For Google Mobile Services 
-keep class com.google.android.gms.** { *; }
// For Huawei Mobile Services 
-keep class com.huawei.hms.**{*;}
```

## **Apps targeted at children**

When developing and publishing apps targeted at children under 13 years old, you need to ensure special conditions for data processing. Any mobile app aimed at children or intended for users in a region with strict regulations on child online protection, must comply with current laws.

{% hint style="info" %}
Please study the following requirements:

* USA: [Children’s Online Privacy Protection Act (COPPA)](https://www.ftc.gov/tips-advice/business-center/privacy-and-security/children%27s-privacy)&#x20;
* EU: [General Data Protection Regulation (GDPR) Article 8](https://gdpr-info.eu/art-8-gdpr/)
  {% endhint %}

If your app has to comply with the legal requirements (COPPA), use the following recommendations:

1. Implement the `coppaControlEnable` method. The method disables collection of ad IDs and vendor IDs.
2. If your app is using Google services, remove the following dependencies from gradle:

   ```
   'com.google.android.gms:play-services-ads-identifier'  
   'com.devtodev:android-google'
   ```
3. If your app is using Huawei services, remove the following dependencies from gradle:

   ```
   'com.huawei.hms:ads-identifier' 
   'com.huawei.hms:opendevice' 
   'com.devtodev.android-huawei'
   ```

{% hint style="warning" %}
Call the `coppaControlEnable` method before SDK initialization. If the method was not called, the SDK will work as before.
{% endhint %}

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
DTDAnalytics.coppaControlEnable()
DTDAnalytics.initialize("App ID", config, context = this)
```

{% endtab %}

{% tab title="Java" %}

```java
DTDAnalytics.INSTANCE.coppaControlEnable();
DTDAnalytics.INSTANCE.initialize("App ID", config, context);
```

{% endtab %}
{% endtabs %}


# iOS

## CocoaPods

[CocoaPods](http://cocoapods.org/) is the easiest way to add devtodev into your iOS project.

1\. Firstly, install CocoaPods using:

```bash
sudo gem install cocoapods
```

2\. In the project directory execute the command:

```bash
pod init
```

3\. In the created Podfile add the dependency:

```bash
platform :ios, '9.0'

target 'TargetName' do
  use_frameworks!
  pod 'DTDAnalytics', '~> 2.0.0'
end
```

4\. Finally, run the command in your Xcode project directory:

```bash
pod install
```

CocoaPods should download and install the devtodev library, and create a new Xcode workspace. Open this workspace in Xcode.

## Manual installation

1\. [Download the latest version of devtodev SDK from the repository  ](https://github.com/devtodev-analytics/ios-sdk-2.0)

2\. Add **`DTDAnalytics.xcframework`** to the project

3\. Add frameworks:

* **`AppTrackingTransparency.framework`**
* **`AdSupport.framework`**

4\. Add initialization t&#x6F;**`didFinishLaunchingWithOptions`** method:

{% tabs %}
{% tab title="Swift" %}

```swift
let config = DTDAnalyticsConfiguration()
config.logLevel = .error
DTDAnalytics.initialize(applicationKey: "App ID", configuration: config)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
DTDAnalyticsConfiguration *config = [[DTDAnalyticsConfiguration alloc] init];
config.logLevel = DTDLogLevelError;
[DTDAnalytics applicationKey:@"App ID" configuration:config];
```

{% endtab %}
{% endtabs %}

You can find the `App ID` in the settings of the respective app in devtodev (Settings → SDK → Integration → [Credentials](/reports-and-functionality/project-related-reports-and-fuctionality/settings#integration)).&#x20;

For [Cross-platform type projects](/getting-started/adding-an-app-to-the-space/cross-platform-application) use `App ID + Platform ID` .

* **`config`** - an object instance of **`DTDAnalyticsConfiguration`**, which is used for specifying additional properties during the initialization.

**`DTDAnalyticsConfiguration`**

| **Parameter**              | **Type**                 | **Description**                                                                                                                                                                                                                                                                                                                                  |
| -------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`currentLevel`**         | int                      | The player level at the moment of devtodev SDK initialization. It is recommended (but optional) to use to improve data precision.                                                                                                                                                                                                                |
| **`userId`**               | string                   | <p>A custom user identifier provided by the developer. If you utilize the default calculation by the device ID, this identifier can be used for finding a user in devtodev.</p><p>In case your project utilizes the calculation by the user identifier, you must set this parameter because it becomes the main user identifier in devtodev.</p> |
| **`trackingAvailability`** | DTDTrackingStatus (enum) | The property allows or disallows devtodev tracking of the user. By default, it is set to ***`DTDTrackingStatus.enable`***. SDK stores the previously assigned value. Pass ***`DTDTrackingStatus.disable`*** if the user opted out of tracking in line with GDPR.                                                                                 |
| **`logLevel`**             | DTDLogLevel (enum)       | The level of logging the SDK activity. The ***`DTDLogLevel.no`*** value is used by default. For troubleshooting during integration it is recommended to set it to ***`DTDLogLevel.debug`***, and either switch it off ***`DTDLogLevel.no`***. Use ***`DTDLogLevel.no`*** in the release version.                                                 |

Example:

{% tabs %}
{% tab title="Swift" %}

```swift
let config = DTDAnalyticsConfiguration()
config.currentLevel = 1
config.userId = "CustomUserID"
config.trackingAvailability = .enable
config.logLevel = .no
DTDAnalytics.initialize(applicationKey: "App ID", configuration: config)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
DTDAnalyticsConfiguration *config;
config.currentLevel = @1;
config.userId = @"CustomUserID";
config.trackingAvailability = DTDTrackingStatusEnable;
config.logLevel = DTDLogLevelNo;
[DTDAnalytics applicationKey:@"App ID" configuration:config];
```

{% endtab %}
{% endtabs %}

### Integration features

#### **For Objective-C**

1. Create Bridging-Header. To do this, you need to add any swift file to the project (don’t delete it later) and choose ‘Create Bridging Header’ in the offered dialog box. &#x20;
2. Make sure that the ‘Build Settings’ for ‘Defines Module’ value evaluates to ‘YES’. &#x20;
3. While importing, use: **`#import <DTDAnalytics/DTDAnalytics-Swift.h>`**

#### **For SwiftUI**&#x20;

For SDK to function properly, it needs to be integrated at the earliest moment of the app launch. It is recommended that you use the following method of main entry point initialization:

```swift
@main
struct TestSwiftUIApp: App {
    init() {
        let config = DTDAnalyticsConfiguration()
        config.logLevel = .debug
        DTDAnalytics.initialize(applicationKey: "App ID", configuration: config)
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
```

## **Apps targeted at children**

When developing and publishing apps targeted at children under 13 years old, you need to ensure special conditions for data processing. Any mobile app aimed at children or intended for users in a region with strict regulations on child online protection, must comply with current laws.

{% hint style="info" %}
Please study the following requirements:

* USA: [Children’s Online Privacy Protection Act (COPPA)](https://www.ftc.gov/tips-advice/business-center/privacy-and-security/children%27s-privacy)&#x20;
* EU: [General Data Protection Regulation (GDPR) Article 8](https://gdpr-info.eu/art-8-gdpr/)
  {% endhint %}

If your app has to comply with the legal requirements (COPPA), use the following recommendations:

1. Implement the `coppaControlEnable` method. The method disables collection of ad IDs and vendor IDs (IDFA, IDFV).
2. To comply with [Apple’s guidelines](https://developer.apple.com/news/?id=091202019a)
   1. Remove `AppTrackingTransparency.framework` and all the links pointing to it.
   2. Remove `AdSupport.framework` all the links pointing to it.

{% hint style="warning" %}
Call the `coppaControlEnable` method before SDK initialization. If the method was not called, the SDK will work as before.
{% endhint %}

{% tabs %}
{% tab title="Swift" %}

```swift
DTDAnalytics.coppaControlEnable()
DTDAnalytics.initialize(applicationKey: "App ID", configuration: config)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[DTDAnalytics coppaControlEnable];
[DTDAnalytics applicationKey:@"App ID" configuration:config];
```

{% endtab %}
{% endtabs %}

## Privacy Manifest

The **Privacy Manifest** is a new way introduced at [WWDC23](https://developer.apple.com/videos/play/wwdc2023/10060/) for third-party SDK developers to provide information about their privacy policies.

The Privacy Manifest describes the methods for ensuring code confidentiality in the application in a unified format. When publishing the application, Xcode will combine the privacy manifests of all third-party SDKs used in your application into a single, convenient report. This report makes it easier to create more accurate privacy labels (Nutrition Labels).

**The Privacy Manifest includes the following sections for data entry:**

* Privacy Tracking Enabled
* Privacy Tracking Domains
* Privacy Nutrition Label Types
* Privacy Accessed API Types

**Privacy Tracking Enabled.** A Boolean value indicating whether the application or third-party SDK uses data for tracking, as defined within the *App Tracking Transparency* framework.

**Privacy Tracking Domains.** An array of strings listing the internet domains that the application or third-party SDK connects to and participates in tracking.

**Privacy Nutrition Label Types.** An array of dictionaries describing the types of data collected by the application or third-party SDK. Nutrition Labels are needed to let users know what data the application collects before installing it from the App Store.

**Privacy Accessed API Types.** An array of dictionaries describing the types of APIs accessed by the application or third-party SDK, which are marked as APIs and require verification for access.

### Privacy manifest for Devtodev SDKs: <a href="#privacy-manifest-for-devtodev-sdks" id="privacy-manifest-for-devtodev-sdks"></a>

### **Analytics module**

**Privacy Nutrition Label Types**

<table><thead><tr><th width="217">Collected Data Type</th><th width="146">Linked to User</th><th width="168">Used for Tracking</th><th>Collection Purposes</th></tr></thead><tbody><tr><td>Product Interaction</td><td>No</td><td>No</td><td>Analytics</td></tr><tr><td>Device ID</td><td>No</td><td>No</td><td>Analytics</td></tr><tr><td>User ID</td><td>No</td><td>No</td><td>Analytics</td></tr><tr><td>Purchase History</td><td>No</td><td>No</td><td>Analytics</td></tr><tr><td>Other Data Types</td><td>No</td><td>No</td><td>Analytics</td></tr></tbody></table>

**Privacy Accessed API Types**

<table><thead><tr><th width="260">Privacy Accessed API Type</th><th>Privacy Accessed API Reasons</th></tr></thead><tbody><tr><td>User Defaults</td><td>CA92.1: Access info from same app, per documentation</td></tr></tbody></table>

### **Messaging module**

**Privacy Nutrition Label Types**

<table><thead><tr><th width="217">Collected Data Type</th><th width="146">Linked to User</th><th width="168">Used for Tracking</th><th>Collection Purposes</th></tr></thead><tbody><tr><td>Device ID</td><td>No</td><td>No</td><td>Analytics</td></tr><tr><td>Other Data Types</td><td>No</td><td>No</td><td>Analytics</td></tr></tbody></table>

**Privacy Accessed API Types**

<table><thead><tr><th width="260">Privacy Accessed API Type</th><th>Privacy Accessed API Reasons</th></tr></thead><tbody><tr><td>User Defaults</td><td>CA92.1: Access info from same app, per documentation</td></tr></tbody></table>

## SDK Signature <a href="#sdk-signature" id="sdk-signature"></a>

Since we distribute our SDKs as binary dependencies, we have implemented a signing practice. Now, when you use a new version of the SDK, Xcode will confirm that it has been signed by us, increasing the integrity of the software supply chain.


# macOS

## Manual installation

1\. [Download the latest version of devtodev SDK from the repository  ](https://github.com/devtodev-analytics/macos-sdk-2.0)

2\. Add **`DTDAnalytics.xcframework`** to the project  (with ***Do Not Embed*** specified)![](blob:https://devtodev.atlassian.net/d3f72139-cd7c-442c-b432-817431ec55a2#media-blob-url=true\&id=922697d7-c4b5-4416-9bea-75ee0236eeb6\&collection=contentId-2520907789\&contextId=2520907789\&mimeType=image%2Fpng\&name=%D0%A1%D0%BD%D0%B8%D0%BC%D0%BE%D0%BA%20%D1%8D%D0%BA%D1%80%D0%B0%D0%BD%D0%B0%202021-05-19%20%D0%B2%2017.39.13.png\&size=40783\&width=628\&height=168)

![](/files/-MgLxU1obl_4w8P4aKIH)

3\. Add frameworks:

* **`AppTrackingTransparency.framework`**
* **`AdSupport.framework`**

4\. Add initialization t&#x6F;**`didFinishLaunchingWithOptions`** method:

{% tabs %}
{% tab title="Swift" %}

```swift
let config = DTDAnalyticsConfiguration()
config.logLevel = .error
DTDAnalytics.initialize(applicationKey: "App ID", configuration: config)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
DTDAnalyticsConfiguration *config;
config.logLevel = DTDLogLevelError;
[DTDAnalytics applicationKey:@"App ID" configuration:config];
```

{% endtab %}
{% endtabs %}

You can find the `App ID` in the settings of the respective app in devtodev (Settings → SDK → Integration → [Credentials](/reports-and-functionality/project-related-reports-and-fuctionality/settings#integration)).&#x20;

For [Cross-platform type projects](/getting-started/adding-an-app-to-the-space/cross-platform-application) use `App ID + Platform ID` .

* **`config`** - an object instance of **`DTDAnalyticsConfiguration`**, which is used for specifying additional properties during the initialization.

**`DTDAnalyticsConfiguration`**

| **Parameter**              | **Type**                 | **Description**                                                                                                                                                                                                                                                                                                                                  |
| -------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`currentLevel`**         | int                      | The player level at the moment of devtodev SDK initialization. It is recommended (but optional) to use to improve data precision.                                                                                                                                                                                                                |
| **`userId`**               | string                   | <p>A custom user identifier provided by the developer. If you utilize the default calculation by the device ID, this identifier can be used for finding a user in devtodev.</p><p>In case your project utilizes the calculation by the user identifier, you must set this parameter because it becomes the main user identifier in devtodev.</p> |
| **`trackingAvailability`** | DTDTrackingStatus (enum) | The property allows or disallows devtodev tracking of the user. By default, it is set to ***`DTDTrackingStatus.enable`***. SDK stores the previously assigned value. Pass ***`DTDTrackingStatus.disable`*** if the user opted out of tracking in line with GDPR.                                                                                 |
| **`logLevel`**             | DTDLogLevel (enum)       | The level of logging the SDK activity. The ***`DTDLogLevel.no`*** value is used by default. For troubleshooting during integration it is recommended to set it to ***`DTDLogLevel.debug`***, and either switch it off ***`DTDLogLevel.no`***. Use ***`DTDLogLevel.no`*** in the release version.                                                 |

Example:

{% tabs %}
{% tab title="Swift" %}

```swift
let config = DTDAnalyticsConfiguration()
config.currentLevel = 1
config.userId = "CustomUserID"
config.trackingAvailability = .enable
config.logLevel = .no
DTDAnalytics.initialize(applicationKey: "App ID", configuration: config)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
DTDAnalyticsConfiguration *config;
config.currentLevel = @1;
config.userId = @"CustomUserID";
config.trackingAvailability = DTDTrackingStatusEnable;
config.logLevel = DTDLogLevelNo;
[DTDAnalytics applicationKey:@"App ID" configuration:config];
```

{% endtab %}
{% endtabs %}

### Integration features

#### **For Objective-C**

1. Create Bridging-Header. To do this, you need to add any swift file to the project (don’t delete it later) and choose ‘Create Bridging Header’ in the offered dialog box. &#x20;
2. Make sure that the ‘Build Settings’ for ‘Defines Module’ value evaluates to ‘YES’. &#x20;
3. While importing, use: **`#import <DTDAnalytics/DTDAnalytics-Swift.h>`**

#### **For SwiftUI**&#x20;

For SDK to function properly, it needs to be integrated at the earliest moment of the app launch. It is recommended that you use the following method of main entry point initialization:

```swift
@main
struct TestSwiftUIApp: App {
    init() {
        let config = DTDAnalyticsConfiguration()
        config.logLevel = .debug
        DTDAnalytics.initialize(applicationKey: "App ID", configuration: config)
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
```

## **Apps targeted at children**

When developing and publishing apps targeted at children under 13 years old, you need to ensure special conditions for data processing. Any mobile app aimed at children or intended for users in a region with strict regulations on child online protection, must comply with current laws.

{% hint style="info" %}
Please study the following requirements:

* USA: [Children’s Online Privacy Protection Act (COPPA)](https://www.ftc.gov/tips-advice/business-center/privacy-and-security/children%27s-privacy)&#x20;
* EU: [General Data Protection Regulation (GDPR) Article 8](https://gdpr-info.eu/art-8-gdpr/)
  {% endhint %}

If your app has to comply with the legal requirements (COPPA), use the following recommendations:

1. Implement the `coppaControlEnable` method. The method disables collection of ad IDs and vendor IDs (IDFA, IDFV).
2. To comply with [Apple’s guidelines](https://developer.apple.com/news/?id=091202019a)
   1. Remove `AppTrackingTransparency.framework` and all the links pointing to it.
   2. Remove `AdSupport.framework` all the links pointing to it.

{% hint style="warning" %}
Call the `coppaControlEnable` method before SDK initialization. If the method was not called, the SDK will work as before.
{% endhint %}

{% tabs %}
{% tab title="Swift" %}

```swift
DTDAnalytics.coppaControlEnable()
DTDAnalytics.initialize(applicationKey: "App ID", configuration: config)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[DTDAnalytics coppaControlEnable];
[DTDAnalytics applicationKey:@"App ID" configuration:config];
```

{% endtab %}
{% endtabs %}


# Windows

{% tabs %}
{% tab title="Universal Windows Platform (UWP)" %}

## 1. NuGet Installation

{% embed url="<https://www.nuget.org/packages/DevToDev.Analytics.Uwp/>" %}

### Package Manager UI

Find the **`DevToDev.Analytics.Uwp`** package using the package manager search engine and click Install. The latest version of the package is recommended.

## 2. SDK Initialization

Initialize the library using the following code:

```csharp
var config = new DTDAnalyticsConfiguration();
config.LogLevel = DTDLogLevel.Error;
DTDAnalytics.Initialize("App ID", config);
```

You can find the `App ID` in the settings of the respective app in devtodev (Settings → SDK → Integration → [Credentials](/reports-and-functionality/project-related-reports-and-fuctionality/settings#integration)).&#x20;

For [Cross-platform type projects](/getting-started/adding-an-app-to-the-space/cross-platform-application) use `App ID + Platform ID` .

* **`config`** - is a **`DTDAnalyticsConfiguration`** object instance that is used for specifying additional properties during initialization.

**`DTDAnalyticsConfiguration`**

| **Parameter**              | **Type**                 | **Description**                                                                                                                                                                                                                                                                                     |
| -------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`currentLevel`**         | Integer                  | The player level at the moment of devtodev SDK initialization. It’s optional but we recommend using it for improving data accuracy.                                                                                                                                                                 |
| **`userId`**               | String                   | A custom user ID assigned by the developer. In the case of default calculation by device IDs, the identifier can be used for searching users in devtodev. In case the project uses calculation by user IDs, the parameter is mandatory because it becomes the principal calculation ID in devtodev. |
| **`trackingAvailability`** | DTDTrackingStatus (enum) | The property allows or disallows devtodev tracking of the user. By default, it is set to **`DTDTrackingStatus.Enable`**. SDK stores the previously assigned value. Pass **`DTDTrackingStatus.Disable`** if the user opted out of tracking in line with GDPR.                                        |
| **`logLevel`**             | DTDLogLevel (enum)       | The level of logging the SDK activity. The ***`DTDLogLevel.no`*** value is used by default. For troubleshooting during integration it is recommended to set it to ***`DTDLogLevel.Debug`***, and either switch it off ***`DTDLogLevel.No`***. Use ***`DTDLogLevel.No`*** in the release version.    |

Example:

```csharp
var config = new DevToDev.Analytics.DTDAnalyticsConfiguration();
config.LogLevel = DTDLogLevel.No;
config.CurrentLevel = 2;
config.UserId = "CustomUserId";
config.TrackingAvailability = DTDTrackingStatus.Enable;
DevToDev.Analytics.DTDAnalytics.Initialize("App ID", config);
```

{% endtab %}

{% tab title=".NET Native" %}

## 1. NuGet Installation

{% embed url="<https://www.nuget.org/packages/DevToDev.Analytics/>" %}

### Package Manager UI

Find the **`DevToDev.Analytics`** package using the package manager search engine and click Install. The latest version of the package is recommended.

## 2. SDK Initialization

Initialize the library using the following code:

```csharp
var config = new DTDAnalyticsConfiguration();
config.LogLevel = DTDLogLevel.Error;
DTDAnalytics.Initialize("App ID", config);
```

You can find the `App ID` in the settings of the respective app in devtodev (Settings → SDK → Integration → [Credentials](/reports-and-functionality/project-related-reports-and-fuctionality/settings#integration)).&#x20;

For [Cross-platform type projects](/getting-started/adding-an-app-to-the-space/cross-platform-application) use `App ID + Platform ID` .

* **`config`** - is a **`DTDAnalyticsConfiguration`** object instance that is used for specifying additional properties during initialization.

**`DTDAnalyticsConfiguration`**

| **Parameter**              | **Type**                 | **Description**                                                                                                                                                                                                                                                                                                                   |
| -------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`currentLevel`**         | Integer                  | The player level at the moment of devtodev SDK initialization. It’s optional but we recommend using it for improving data accuracy.                                                                                                                                                                                               |
| **`userId`**               | String                   | A custom user ID assigned by the developer. In the case of default calculation by device IDs, the identifier can be used for searching users in devtodev. In case the project uses calculation by user IDs, the parameter is mandatory because it becomes the principal calculation ID in devtodev.                               |
| **`trackingAvailability`** | DTDTrackingStatus (enum) | The property allows or disallows devtodev tracking of the user. By default, it is set to ***`DTDTrackingStatus.Enabl`*****`e`**. SDK stores the previously assigned value. Pass ***`DTDTrackingStatus.Disable`*** if the user opted out of tracking in line with GDPR.                                                            |
| **`logLevel`**             | DTDLogLevel (enum)       | The level of logging the SDK activity. The ***`DTDLogLevel.No`*** value is used by default. For troubleshooting during integration, it is recommended to set it to ***`DTDLogLevel.Debug`***, and either switch it off ***`DTDLogLevel.No`*** or use it only for error handling ***`DTDLogLevel.Error`*** in the release version. |
| **`ApplicationVersion`**   | String                   | The app version during the devtodev SDK initialization. It is recommended that you set the app version before the initialization to make the collection of app version statistics more precise.                                                                                                                                   |

Example:

```csharp
var config = new DevToDev.Analytics.DTDAnalyticsConfiguration();
config.LogLevel = DTDLogLevel.Error;
config.CurrentLevel = 2;
config.UserId = "CustomUserId";
config.TrackingAvailability = DTDTrackingStatus.Enable;
config.ApplicationVersion = "1.2.34";
DevToDev.Analytics.DTDAnalytics.Initialize("App ID", config);
```

## **3. SDK Activity**

The SDK can’t control app activity hence this responsibility is passed on to the developer. During the SDK initialization, the activity is triggered automatically, and later the activity status will not change automatically. For tracking app activity, the developer can use the **`DTDAnalytics.StartActivity`** and **`DTDAnalytics.StopActivity`** methods. It is recommended that you use the **`DTDAnalytics.StopActivity`** method to stop the activity when the app goes into the background or being closed. If the window is re-opened from the taskbar it is recommended to renew the activity by using the **`DTDAnalytics.StartActivity`** method.<br>
{% endtab %}
{% endtabs %}


# Web


# Web SDK Integration

{% hint style="success" %}
The latest version of the Web SDK: <mark style="color:red;">**3.0**</mark>

Please see the [**changelog**](/integration/integration-of-sdk-v2/sdk-integration/web/web-sdk-releases) if you are using an outdated version.
{% endhint %}

## Integration

### Installation via NPM package

```bash
npm install @dev-2-dev/websdk
```

### CDN installation

To integrate SDK, add the following line to the tag of your page:

```html
<script src="https://cdn.devtodev.com/sdk/web/v3/devtodevsdk.js"></script>
```

The SDK will be available globally as `window.devtodev`.

## Initialization

[Add the application](/getting-started/adding-an-app-to-the-space) to the Space using the wizard for adding applications.

In order for SDK for WEB to start working, it is necessary to perform initialization right after the page is loaded and you have a basic user identifier at your disposal.

#### Using NPM package&#x20;

```javascript
import DTDAnalytics from '@dev-2-dev/websdk';

// Create an instance
const analytics = new DTDAnalytics();

// Initialize with your app ID
analytics.initialize('App ID', config);
```

#### Using CDN&#x20;

```html
<!DOCTYPE html>
<html>
  <head>
    <script src="https://cdn.devtodev.com/sdk/web/v3/devtodevsdk.js"></script>
  </head>
  <body>
    <script>
      const analytics = window.devtodev;

      // Initialize with your app ID
      analytics.initialize('App ID', config);
    </script>
  </body>
</html>
```

You can find the `App ID` in the settings of the respective app in devtodev (Settings → SDK → Integration → [Credentials](/reports-and-functionality/project-related-reports-and-fuctionality/settings#integration)).&#x20;

For [Cross-platform type projects](/getting-started/adding-an-app-to-the-space/cross-platform-application) use `App ID + Platform ID` .

**`config`** – is an object that is used for specifying additional properties during initialization.

{% hint style="info" %}
Since there’s no option to get any consistent identifier in web browsers, we recommend using as a User ID either a social network ID with your app or an ID that your server assigns to a user. It’s best to assign a User ID and specify it in the **`config`** object during the SDK initialization instead of using a [**`setUserId`**](/integration/integration-of-sdk-v2/setting-up-events/user-profile#user-id) method after the initialization.

If you have a game app, we recommend specifying the current player’s level either in the **`config`** or at the earliest possible moment after the initialization via the [**`setCurrentLevel`**](/integration/integration-of-sdk-v2/setting-up-events/user-profile#current-user-level) method.
{% endhint %}

### **`Config`**

<table><thead><tr><th width="273.45508982035926">Parameter</th><th width="150">Type</th><th>Description</th></tr></thead><tbody><tr><td><strong><code>userId</code></strong></td><td>string</td><td><p>Unique user identifier. For example, user’s ID in a social network, or a unique account name used for user identification on your server. If at the time of initialization this identifier is not yet available, specify the identifier later using </p><p> the <strong><code>setUserId</code></strong> method.</p></td></tr><tr><td><strong><code>currentLevel</code></strong></td><td>integer</td><td>The player level at the moment of devtodev SDK initialization. Must be greater than 0. It’s optional but we recommend using it for improving data accuracy.</td></tr><tr><td><strong><code>trackingAvailability</code></strong></td><td>boolean</td><td>The property allows or disallows devtodev tracking of the user. By default, it is set to <em><strong><code>true</code></strong></em>. SDK stores the previously assigned value. Pass false if the user opted out of tracking in line with GDPR.</td></tr><tr><td><strong><code>logLevel</code></strong></td><td>string</td><td>The level of logging the SDK activity. The "<em><strong><code>No</code></strong></em>" value is used by default. For troubleshooting during integration, it is recommended to set it to "<em><strong><code>Debug</code></strong></em>", and either switch it "<em><strong><code>No</code></strong></em>" or use it only for error handling "<em><strong><code>Error</code></strong></em>" in the release version.</td></tr><tr><td><strong><code>applicationVersion</code></strong></td><td>String</td><td>The app version. Cannot be empty.</td></tr></tbody></table>

Example:

```javascript
const config = {};
config.userId = "Unique user identifier";
config.currentLevel = 2;
config.trackingAvailability = true;
config.logLevel = "Error";
config.applicationVersion = "1";
analytics.initialize("App ID", config);
```

## Session recording&#x20;

A powerful session recording plugin that automatically captures user interactions on your website using advanced screen recording technology. This plugin integrates seamlessly with the DevToDev WebSDK to provide comprehensive session replay capabilities.&#x20;

{% content-ref url="/pages/rVyDlIAOfcLisVZk8scm" %}
[Session replays](/reports-and-functionality/project-related-reports-and-fuctionality/users/session-replays)
{% endcontent-ref %}

### Installation&#x20;

#### NPM

```bash
npm install @dev-2-dev/websdk-plugin-session-tracker
```

#### Yarn&#x20;

```bash
yarn add @dev-2-dev/websdk-plugin-session-tracker
```

### Enabling the plugin&#x20;

#### NPM

```javascript
import DTDAnalytics from '@dev-2-dev/websdk';
import DTDWebSessionTracker from '@dev-2-dev/websdk-plugin-session-tracker';

const sessionTracker = new DTDWebSessionTracker();
const analytics = new DTDAnalytics();

analytics.initialize('App ID');
analytics.registerPlugin(sessionTracker);
```

#### CDN&#x20;

```html
<script src="https://cdn.devtodev.com/sdk/web/v3/devtodevsdk.js"></script>
<script src="https://cdn.devtodev.com/sdk/web/v3/plugins/session-tracker/websdk-plugin-session-tracker.js"></script>

<script>
  const analytics = window.devtodev;
  const sessionTracker = new DTDWebSessionTracker();

  // Initialize with your app ID
  analytics.initialize('App ID', config);
  analytics.registerPlugin(sessionTracker);
</script>
```

#### Disabling from SDK Config

You can disable session recording through the SDK's `webSessions` configuration:

```javascript
import { DTDAnalytics } from '@dev-2-dev/websdk';

const analytics = new DTDAnalytics({
  appId: 'App ID',
  webSessions: {
    enabled: false, // Disable session recording
  },
});
```

### Session recording configuration&#x20;

#### Recording Options (Constructor Config)

Configure privacy and recording behavior when creating the tracker instance:

```javascript
import { DTDWebSessionTracker } from '@dev-2-dev/websdk-plugin-session-tracker';

const sessionRecordingConfig = {
  recordCanvas: true, // Record canvas elements
  maskAllInputs: false, // Mask all input fields
  maskInputOptions: {
    password: true, // Always mask password fields (default: true)
    email: true, // Mask email fields (default: true)
    text: false, // Don't mask text inputs (default: false)
  },
  blockClass: 'sensitive-field', // Block elements with this class
};

const sessionTracker = new DTDWebSessionTracker(sessionRecordingConfig);
```

#### Configuration Options

These options are passed to the `DTDWebSessionTracker` constructor:

| Option             | Type    | Default             | Description                                     |
| ------------------ | ------- | ------------------- | ----------------------------------------------- |
| `recordCanvas`     | boolean | `true`              | Record canvas elements                          |
| `maskAllInputs`    | boolean | `false`             | Mask all input fields                           |
| `maskInputOptions` | object  | See below           | Selective input masking options                 |
| `blockClass`       | string  | `'sensitive-field'` | CSS class name to block elements from recording |

**Mask Input Options:**

| Option     | Type    | Default | Description                 |
| ---------- | ------- | ------- | --------------------------- |
| `password` | boolean | `true`  | Always mask password fields |
| `email`    | boolean | `true`  | Mask email fields           |
| `text`     | boolean | `false` | Mask text inputs            |

### Privacy configuration examples&#x20;

1. Mask all inputs:

```javascript
const sessionTracker = new DTDWebSessionTracker({
  maskAllInputs: true,
});
```

2. Selective masking:&#x20;

```javascript
const sessionTracker = new DTDWebSessionTracker({
  maskAllInputs: false,
  maskInputOptions: {
    password: true,  // Mask passwords
    email: true,     // Mask emails
    text: false,     // Don't mask text inputs
  },
});
```

3. Block specific elements. Add the `'sensitive-field'` class to exclude elements from recording:&#x20;

```html
<div class="sensitive-field">
  <p>This content will not be recorded</p>
</div>

<input type="password" class="sensitive-field" />
<input type="email" class="sensitive-field" />
```


# Web SDK Releases

Changelog for WEB SDK

## Version 3.0 (3 Feb 2026)&#x20;

* [Remote configuration](/integration/integration-of-sdk-v2/remote-configuration/rc-integration), simplified A/B testing integration&#x20;
* [Session recording](/reports-and-functionality/project-related-reports-and-fuctionality/users/session-replays)&#x20;
* Updated method calls (See [Setting up Events](/integration/integration-of-sdk-v2/setting-up-events))

## Version 2.2 (28 Mar 2025)

* [Added A/B test functionality](/integration/integration-of-sdk-v2/a-b-testing) (Beta).

## Version 2.1 (06 Dec 2024)

* [Reserved user properties](/integration/integration-of-sdk-v2/setting-up-events/user-profile#reserved-user-properties), which carry information about the user's personal data, are excluded from the SDK.


# Unity

{% hint style="success" %}
Check out [**AI-assisted integration**](/integration/integration-of-sdk-v2/sdk-integration/unity/ai-assisted-integration-beta) to streamline the process.&#x20;
{% endhint %}

## SDK Integration

{% hint style="info" %}
If you have previously used the Unity SDK version 2+, you need to delete the following files and folders in the "Assets" folder of your project:

![](/files/MvBZe4B5DyRzaSHS8HiU)
{% endhint %}

To integrate devtodev analytics SDK, you can use one of the two methods: by using the *Unity Package Manager* (recommended) or by manually importing the *unitypackage*.

### **Integration by using the Unity Package Manager**

{% hint style="warning" %}
If you integrated the devtodev package manually, then you need to delete the Assets/DevToDev and Plugins/DevToDev folders.
{% endhint %}

1. Open the Package Manager (Window → Package Manager), click + in the top left corner and select *Add package from git URL*.
2. Copy the repository URL <https://github.com/devtodev-analytics/package_Analytics.git> to the input box and click *Add*.

   <div align="left"><figure><img src="/files/UnibaMk7iFK7B1a33gjc" alt=""><figcaption></figcaption></figure> <figure><img src="/files/6Ls1duwUv0Yrn3phh6jq" alt=""><figcaption></figcaption></figure></div>
3. Wait for the Unity Package Manager to download the package.&#x20;
4. For Android projects, add an identification package:

{% tabs %}
{% tab title="Android Google" %}
{% hint style="warning" %}
If you work with SDK version 3.5.0 and above, and you want to use Google Ad ID, you need to add [devtodev-analytics/package\_Google](https://github.com/devtodev-analytics/package_Google.git). When developing and publishing apps for kids [(COPPA)](https://www.ftc.gov/tips-advice/business-center/privacy-and-security/children%27s-privacy), you do not need [devtodev-analytics/package\_Google](https://github.com/devtodev-analytics/package_Google.git). Read more about working with [COPPA](https://www.ftc.gov/tips-advice/business-center/privacy-and-security/children%27s-privacy) in the [COPPA section](#apps-targeted-at-children).
{% endhint %}
{% endtab %}

{% tab title="Android Huawei" %}
{% hint style="warning" %}
If you work with SDK version 3.5.0 and above, and you want to use Huawei Ad ID, you need to add [https://github.com/devtodev-analytics/package\_Huawei.git](https://github.com/devtodev-analytics/package_Huawei). When developing and publishing apps for kids [(COPPA)](https://www.ftc.gov/tips-advice/business-center/privacy-and-security/children%27s-privacy), you do not need [https://github.com/devtodev-analytics/package\_Huawei.git](https://github.com/devtodev-analytics/package_Huawei). Read more about working with [COPPA](https://www.ftc.gov/tips-advice/business-center/privacy-and-security/children%27s-privacy) in the [COPPA section](#apps-targeted-at-children).
{% endhint %}
{% endtab %}
{% endtabs %}

{% hint style="info" %}
You can pick a specific SDK version by adding # and a version number at the end of the URL, for example: <https://github.com/devtodev-analytics/package_Analytics.git>#v3.3.2&#x20;
{% endhint %}

### **Integration by importing&#x20;*****unitypackage***

1. Download *DTDAnalytics.unitypackage* from <http://github.com/devtodev-analytics/Unity-sdk-3.0/releases/latest>.
2. In the Unity Editor menu, open Assets → Import Package → Custom Package.
3. Select the DTDAnalytics.unitypackage that you have just downloaded.
4. Click Import.&#x20;
5. For Android projects, import an identification package:

{% tabs %}
{% tab title="Android Google" %}
{% hint style="warning" %}
If you work with SDK version 3.5.0 and above, and you want to use Google Ad ID, you need to import the DTDGoogle.*unitypackage*. When developing and publishing apps for kids [(COPPA)](https://www.ftc.gov/tips-advice/business-center/privacy-and-security/children%27s-privacy), you do not need the DTDGoogle.*unitypackage*. Read more about working with [COPPA](https://www.ftc.gov/tips-advice/business-center/privacy-and-security/children%27s-privacy) in the [COPPA section](#apps-targeted-at-children).
{% endhint %}
{% endtab %}

{% tab title="Android Huawei" %}
{% hint style="warning" %}
If you work with SDK version 3.5.0 and above, and you want to use Huawei Ad ID, you need to import the DTDHuawei.*unitypackage*. When developing and publishing apps for kids [(COPPA)](https://www.ftc.gov/tips-advice/business-center/privacy-and-security/children%27s-privacy), you do not need the DTDHuawei.*unitypackage*. Read more about working with [COPPA](https://www.ftc.gov/tips-advice/business-center/privacy-and-security/children%27s-privacy) in the [COPPA section](#apps-targeted-at-children).
{% endhint %}
{% endtab %}
{% endtabs %}

## SDK Initialization

Create a script with the following code and attach it to the **`GameObject`** that will survive the entire life cycle of the app.

```csharp
using DevToDev.Analytics;
using UnityEngine;

public class DTDObject : MonoBehaviour
{
    void Start()
    { 
#if UNITY_ANDROID
        DTDAnalytics.Initialize("Android App ID");
#elif UNITY_IOS
        DTDAnalytics.Initialize("iOS App ID");
#elif UNITY_WEBGL
        DTDAnalytics.Initialize("Web App ID");
#elif UNITY_STANDALONE_WIN
        DTDAnalytics.Initialize("Windows App ID");
#elif UNITY_STANDALONE_OSX
        DTDAnalytics.Initialize("OSX App ID");
#elif UNITY_WSA
        DTDAnalytics.Initialize("UWP App ID");
#endif
    }
}
```

You can find the `App ID` in the settings of the respective app in devtodev (Settings → SDK → Integration → [Credentials](/reports-and-functionality/project-related-reports-and-fuctionality/settings#integration)).&#x20;

For [Cross-platform type projects](/getting-started/adding-an-app-to-the-space/cross-platform-application) use `App ID + Platform ID` .

**`config`** - an object instance of **`DTDAnalyticsConfiguration`**, which is used for specifying additional properties during the initialization.

**`DTDAnalyticsConfiguration`**

<table data-header-hidden><thead><tr><th width="261.3333333333333">Parameter</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><strong>Parameter</strong></td><td><strong>Type</strong></td><td><strong>Description</strong></td></tr><tr><td><strong><code>CurrentLevel</code></strong></td><td>Integer</td><td>The player level at the moment of devtodev SDK initialization. It’s optional but we recommend using it for improving data accuracy.</td></tr><tr><td><strong><code>UserId</code></strong></td><td>String</td><td>A custom user ID assigned by the developer. In the case of default calculation by device IDs, the identifier can be used for searching users in devtodev. In case the project uses calculation by user IDs, the parameter is mandatory because it becomes the principal calculation ID in devtodev.</td></tr><tr><td><strong><code>TrackingAvailability</code></strong></td><td>DTDTrackingStatus (enum)</td><td>The property allows or disallows devtodev tracking of the user. By default, it is set to <em><strong><code>DTDTrackingStatus.Enable</code></strong></em>. SDK stores the previously assigned value. Pass <em><strong><code>DTDTrackingStatus.Disable</code></strong></em> if the user opted out of tracking in line with GDPR.</td></tr><tr><td><strong><code>LogLevel</code></strong></td><td>DTDLogLevel (enum)</td><td>The level of logging the SDK activity. The <em><strong><code>DTDLogLevel.no</code></strong></em> value is used by default. For troubleshooting during integration it is recommended to set it to <em><strong><code>DTDLogLevel.Debug</code></strong></em>, and either switch it off <em><strong><code>DTDLogLevel.No</code></strong></em>. Use <em><strong><code>DTDLogLevel.No</code></strong></em> in the release version.</td></tr><tr><td><strong><code>ApplicationVersion</code></strong></td><td>String</td><td>The app version during the devtodev SDK initialization. Use the property on the WinStandalone platform only. For <strong>all other platforms, data is collected automatically.</strong></td></tr></tbody></table>

Example:

```csharp
var config = new DTDAnalyticsConfiguration
{
    ApplicationVersion = "1.2.3",
    LogLevel = DTDLogLevel.No,
    TrackingAvailability = DTDTrackingStatus.Enable,
    CurrentLevel = 1,
    UserId = "unique_userId"
};
```

```csharp
using DevToDev.Analytics;
using UnityEngine;

public class DTDObject : MonoBehaviour
{
    void Start()
    { 
#if UNITY_ANDROID
        DTDAnalytics.Initialize("androidAppID", config);
#elif UNITY_IOS
        DTDAnalytics.Initialize("iOSAppID", config);
#elif UNITY_WEBGL
        DTDAnalytics.Initialize("WebAppID", config);
#elif UNITY_STANDALONE_WIN
        DTDAnalytics.Initialize("winAppID", config);
#elif UNITY_STANDALONE_OSX
        DTDAnalytics.Initialize("OSXAppID", config);
#elif UNITY_WSA
        DTDAnalytics.Initialize("UwpAppID", config);
#endif
    }
}
```

## Specific integration features of certain platforms

### Windows Standalone

**SDK Activity**

The SDK can’t control app activity in case you use Windows Standalone therefore this responsibility is shifted to the developer. While initializing the SDK, the activity starts automatically and after that, the activity status will not auto-change. To track app activity, the developer can use the following methods: **`DTDAnalytics.StartActivity`** and **`DTDAnalytics.StopActivity`**. It is recommended to use the **`DTDAnalytics.StopActivity`** method to stop activity when the app goes into the background or gets closed. You can use the **`DTDAnalytics.StartActivity`** method to resume activity when the app gets reopened from the taskbar.

For other platforms, there is no need to manually call the **`DTDAnalytics.StartActivity`** and **`DTDAnalytics.StopActivity`** methods.

### Universal Windows Platform (WSA) <a href="#universal-windows-platform-wsa" id="universal-windows-platform-wsa"></a>

![](/files/1n582p05LBpbzLoZ4IUx)

{% hint style="info" %}
In the version 2019.2, the Executable Only option was added to the Build Type. This option is incompatible with the SDK because the SDK needs access to the project file in order to register the **`DevToDev.Background`** service.
{% endhint %}

### Android

{% hint style="warning" %}
To resolve external android dependencies, you need to use [External Dependency Manager for Unity](https://github.com/googlesamples/unity-jar-resolver/releases/latest).
{% endhint %}

Add the following strings to **proguard.txt** ([read more about Unity proguard here](https://docs.unity3d.com/Manual/android-gradle-overview.html)):

```kotlin
-keep class com.devtodev.** { *; }
-dontwarn com.devtodev.**
```

### Huawei

1. Select your project in <https://developer.huawei.com/consumer/en/service/josp/agc/index.html#/myProject> \
   In the “General information” section find “App information” and download the “agconnect-services.json“ file.<br>

   <figure><img src="/files/ZhfpZcajgWzJpQQMhbTP" alt=""><figcaption></figcaption></figure>

2. If you imported the DTDGoogle package, delete the imported files Assets\Plugins\DevToDev\Android\DTDGoogleAndroid.dll and Assets\DevToDev\Analytics\Editor\GoogleDependencies.xml<br>

   <figure><img src="/files/1KvTmr7W2TujkgK1vT1L" alt=""><figcaption></figcaption></figure>

3. Import the DTDHuawei.unitypackage manually from [GitHub](https://github.com/devtodev-analytics/Unity-sdk-3.0) or use Unity Package Manager with [DTDHuawei package](https://github.com/devtodev-analytics/package_Huawei). <br>

   <figure><img src="https://lh6.googleusercontent.com/fMoVg2IVKla7MwYlg96evCSHHjidhYGIqw4Y-_JQCx6FVs0tH4JTGNOeyCkDqm1LAjpqtP6w80C859f-VFlzFoi8istEzUW_mR_6mKC2DLF1_oYeO6dXYSFNqH9Ca_iNE88hpPPKVuDxQ0QxiFwMG8o" alt=""><figcaption></figcaption></figure>

4. In the assets/Plugins/Android/ folder create a settingsTemplate.gradle file with the following content:

   ```groovy
   import java.nio.file.Files

   static void enableJetifier(Project project) {
       project.ext['android.useAndroidX'] = true
       project.ext['android.enableJetifier'] = true
   }

   static void addBuildscript(Project project) {
       project.buildscript {
           repositories {
               maven { url 'https://plugins.gradle.org/m2/' } 
               maven { url 'https://developer.huawei.com/repo/' }
           }

           dependencies {
               classpath 'com.huawei.agconnect:agcp:1.6.5.300'
           }
       }
   }

   static void applyPlugins(Project project) {
       if (project.name != 'launcher') return

       project.afterEvaluate {
           it.apply plugin: 'com.huawei.agconnect'
       }
   }

   static void copyAppGalleryJson(Project project) {
       if (project.name != 'launcher') return

       def destinationFile = new File("${project.rootDir}/launcher/agconnect-services.json")
       if (destinationFile.exists()) return

       def sourceFile = new File("${project.rootDir}/unityLibrary/devtodev.plugin/agconnect-services.json")
       Files.copy(sourceFile.toPath(), destinationFile.toPath())
   }

   gradle.rootProject {
       it.afterEvaluate {
           it.allprojects {
               enableJetifier(it)
               addBuildscript(it)
               applyPlugins(it)
               copyAppGalleryJson(it)
           }
       }
   }

   include ':launcher', ':unityLibrary'
   **INCLUDES**
   ```

5. Open Window → devtodev and select Create android plugin folder

   <figure><img src="https://lh5.googleusercontent.com/R-JwAN2Ab7RoBNmuHyP2DQpvdCF3g6rT3GYO72SlkxEIU9CgZJ90PisLsnuONnUHyTU7WB5dEQp0WbPyDcOWJ66XO-vLjs9C9lp2jKUGREJFAG4-bRQFJmhNZNFj038BbIRZiJXsP3-z25eiezwLfC4" alt=""><figcaption></figcaption></figure>

6. Copy the “agconnect-services.json“ file to the Assets\Plugins\Android\devtodev.plugin folder

   <figure><img src="https://lh4.googleusercontent.com/Mge1D3wgKw59xobvI-alRU6Hq9nTqWU5LZru0F2XnJHnRMdDQu8zQCyTH9Nv-rjsg6dp_r7gPkHxuD3D2whgnySRJAqLLsN0GF6C6tvuZv9WH4SJm4XKQTqKBOXu_u1LEs2BUyPj9wYiR5wm_gDTx3w" alt=""><figcaption></figcaption></figure>

7. Open Assets → External Dependency Manager → Android ResolverAssets → External Dependency Manager → Android Resolver and click Resolve<br>

   <figure><img src="https://lh5.googleusercontent.com/e0ZAoxCh5Nq11UljtopTU6pu19Tm5ECJE0bsem-Y3rC1MQE8slj1fvh752nToW6OKy60HAs_vHuY_b1ssLWy5lNe7OQqiwkKbbFOvWFRaYkkVLOpyvPk_39gm9jsgLWRHneV5JLqTvNLgW31GOUh5EY" alt=""><figcaption></figcaption></figure>

8. &#x20;To [proguard](https://docs.unity3d.com/Manual/android-gradle-overview.html) add the following rule:

   ```
   -keep class com.devtodev.** { *; }
   -dontwarn com.devtodev.**
   -keep class com.huawei.hms.**{*;}
   ```

### iOS

To integrate with Xcode, the SDK uses **`PostProcessBuild`** in the **`DTDPostProcessAnalytics`** and **`DTDPostProcessMessaging`** scripts. If you use custom **`PostProcessBuild`** scripts, add them **`callbackOrder`** of less than ***`98`*** to avoid conflicts.&#x20;

{% hint style="info" %}
Your app must **request** tracking authorization before it can get the advertising identifier. See [the detailed instruction](https://docs.unity.com/ads/ATTCompliance.html) on how to request it.
{% endhint %}

{% hint style="info" %}
If you want to disable tracking of advertising identifiers, you need to open the ***DTDPostProcesssAnalytics.cs*** file and comment out the line ***39**:*\
**project.AddFrameworkToProject(targetGuid, "AppTrackingTransparency.framework", true);**
{% endhint %}

#### iOS SDK Signature and Privacy Manifest

At [WWDC23](https://developer.apple.com/videos/play/wwdc2023/10060/) Apple introduced new privacy manifests and xcframework signature. More information about it can be found [here](/integration/integration-of-sdk-v2/sdk-integration/ios#privacy-manifest).

## **Apps targeted at children**

When developing and publishing apps targeted at children under 13 years old, you need to ensure special conditions for data processing. Any mobile app aimed at children or intended for users in a region with strict regulations on child online protection, must comply with current laws.

{% hint style="info" %}
Please study the following requirements:

* USA: [Children’s Online Privacy Protection Act (COPPA)](https://www.ftc.gov/tips-advice/business-center/privacy-and-security/children%27s-privacy)&#x20;
* EU: [General Data Protection Regulation (GDPR) Article 8](https://gdpr-info.eu/art-8-gdpr/)
  {% endhint %}

If your app has to comply with the legal requirements (COPPA), use the following recommendations:

{% tabs %}
{% tab title="Apple" %}

* Implement the `CoppaControlEnable` method. The method disables collection of ad IDs and vendor IDs (IDFA, IDFV).
* To comply with [Apple’s guidelines](https://developer.apple.com/news/?id=091202019a), remove from Xcode project:
  1. `AppTrackingTransparency.framework` and all the links pointing to it.
  2. `AdSupport.framework` and all the links pointing to it.
  3. Depending on the SDK version:
     1. 3.9.0 and older:\
        Set `IS_COPPA_ENABLED = true` in `DTDPostProcessAnalytics.cs` (`Assets/DevToDev/Analytics/Editor`)
     2. 3.9.1 and newer:\
        Add a new [Scripting Define Symbols](https://docs.unity3d.com/6000.0/Documentation/Manual/custom-scripting-symbols.html): DTD\_COPPA
        {% endtab %}

{% tab title="Android" %}

1. Implement the `CoppaControlEnable` method. The method disables collection of ad IDs and vendor IDs.
2. If you are using DTDGoogle or DTDHuawei from Unity Package Manager, disable it.&#x20;
3. If you are using DTDGoogle.unitypackage, remove the following files:

   ```
   Assets\DevToDev\Analytics\Editor\GoogleDependencies.xml
   Assets\Plugins\DevToDev\Android\DTDGoogleAndroid.aar
   ```
4. If you are using DTDHuawei.unitypackage, remove the following files:

   ```
   Assets\Plugins\Android\settingsTemplate.gradle
   Assets\Plugins\DevToDev\Android\DTDHuaweiAndroid.aar
   Assets\DevToDev\Analytics\Editor\HuaweiDependencies.xml
   ```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Call the `CoppaControlEnable` method before SDK initialization. If the method was not called, the SDK will work as before.
{% endhint %}

```csharp
DTDAnalytics.CoppaControlEnable();
DTDAnalytics.Initialize("App ID", config);
```


# AI-assisted integration (Beta)

Use AI assistance to speed up the integration process.&#x20;

{% hint style="info" %}
**Requirements**:

1. Project built with Unity.&#x20;
2. The project does not have devtodev integration yet. \
   Projects with poorily integrated events can also participate.
3. AI coding agent that have access to the complete application code.&#x20;
   {% endhint %}

If you have any questions regarding the integration, let us know by using the [`Contact Us`](https://www.devtodev.com/contact-us) form or reach out to our Customer Success team directly within the platform. We will also greatly appreciate your feedback. \
Please add **AI INTEGRATION** when submitting your request.&#x20;

{% hint style="success" %}
We created this manual for [**Cursor**](https://www.cursor.com/) AI code editor but you can try using other AI coding agents that can access all of application code files.&#x20;
{% endhint %}

{% embed url="<https://youtu.be/WZh78nSMc48>" %}
This example is for game application made with Unity, however, the process is similar for other applications types
{% endembed %}

## Preparation and prompt

### Get **Unity Integration Guide File**

Download the **Unity Integration Guide File** and add it to your Unity project folder. \
We recommend creating a folder called `docs` and placing the guide inside it. This ensures the **AI coding agent** can easily access and use it during analysis.&#x20;

{% file src="/files/bnSoVLaxJpUhbUN92i6U" %}
Unity Integration Guide File
{% endfile %}

Open your AI-agent, and select your Unity project folder. The agent will begin indexing all project files, including the markdown guide. Wait a few moments until indexing is complete. &#x20;

### Create prompt&#x20;

Create a **clear and effective prompt** for the AI-agent.\
The prompt should:

* Assign the AI a role (like "You are a product engineer").
* Specify your goal: integrate DevToDev events properly.
* Reference the guide using `@devtodev_unity_integration_guide.md`.

Copy and paste the prompt text into the AI-agent chat window.&#x20;

You can use our [**prompt**](#get-unity-integration-guide-file) as an example.&#x20;

{% hint style="info" %}
The prompt accuracy is estimated at **around 90%**, which means that some devtodev method calls may contain errors. Therefore, it’s recommended that developers review the integration code.
{% endhint %}

### Add references to integration guide

Attach the guide file inside the prompt. You can do this in two ways:

1. Find all the guide mentions in the prompt and delete that placeholder text. Then, drag the file from the `docs` folder into the chat window. The AI-agent will automatically recognize and link the file.&#x20;
2. Alternatively, type `@` and start typing "devtodev" — the agent will suggest the guide file. Usually it is at the top of the list.&#x20;

<figure><img src="/files/EixvLZcpcA8q3icTWnVw" alt=""><figcaption></figcaption></figure>

Now click **`Send`** and let the AI coding agent generate the integration.

It will:

* Analyze your codebase
* Understand game logic
* Suggest where and how to insert DevToDev analytics events.

## Events generation&#x20;

Check and review the list of events generated by the AI-agent. You can edit the list by removing the unnecessary or duplicate events. You can also ask the AI to revise its suggestions.&#x20;

After you have finished reviewing and modifying the suggested events file, you can proceed to the next step of integration.&#x20;\
The AI-agent may then generate a new class — usually something like `AnalyticsManager` — to send events to Devtodev.

## Further integration&#x20;

The coding agent might show some errors if Unity has not recompiled yet. Switch to the **Unity Editor** and wait for it to finish compiling. Once done, the errors in AI-agent will disappear.

{% hint style="warning" %}
The AI **must not change** your original game logic.

It may **add** analytics calls (like `DTDAnalytics.CustomEvent(...)`), but should never modify player behavior, game flow, or state handling.

If something looks wrong, ask the AI-agent to revise it.
{% endhint %}

After generating the integration, the AI-agent will also produce a **documentation file** (like `ANALYTICS_SETUP.md`).

It describes:

* Where events were added
* How to use the generated `AnalyticsManager` class
* What next steps are required to finish setup.

Follow that instruction carefully to complete your devtodev analytics integration properly.


# Unreal Engine

## Plugin installation

The SDK is available in [**GitHub repository**](https://github.com/devtodev-analytics/unreal-sdk-2.0). Download the [**Source code (zip)**](https://github.com/devtodev-analytics/unreal-sdk-2.0/releases/latest) of latest release. Unzip the archive and copy **DTDAnalytics** folder to the **Plugins** folder of your project.

For a C++ project type, add the **`DTDAnalytics`** name to the list of dependency module names to the ***\<module\_name>.Build.cs*** file of the module in which you plan to use the plugin.

Example:

```cpp
PublicDependencyModuleNames.Add("DTDAnalytics");
```

## Data types

### class UDTDAnalyticsLibrary

A class that implements analytic methods.

Header file:

```cpp
#include "DTDAnalytics/Public/DTDAnalyticsBPLibrary.h"
```

### class UDTDUserCardLibrary

A class that implements user card methods.

The class header:

```cpp
#include "DTDAnalytics/Public/DTDUserCardBPLibrary.h"
```

### enum class EDTDTrackingStatus : uint8

SDK tracking status.

Header file:

```cpp
#include "DTDAnalytics/Public/DTDTrackingStatus.h"
```

Values:

* `Unknown = 0` - leave tracking unchanged
* `Enable = 1` - tracking enabled
* `Disable = 2` - tracking disabled

Example:

```cpp
EDTDTrackingStatus TrackingStatus = EDTDTrackingStatus::Enable;
```

### enum class EDTDLogLevel : uint8

SDK logging level.

Header file:

```cpp
#include "DTDAnalytics/Public/DTDLogLevel.h"
```

Values:

* `Unknown = 0` - leave logging level unchanged
* `No = 1` - logging disabled
* `Error = 2` - logging of errors
* `Warning = 3` - logging of warnings and errors
* `Info = 4` - logging of information messages, warnings and errors
* `Debug = 5` - logging of debugging messages, informational messages, warnings and errors

Example:

```cpp
EDTDLogLevel LogLevel = EDTDLogLevel::Info;
```

### enum class EDTDAccrualType : uint8

Types of resource accumulation.

Header file:

```cpp
#include "DTDAnalytics/Public/DTDAccrualType.h"
```

Values:

* `Earned = 0` - earned resources
* `Bought = 1` - purchased resources

Example:

```cpp
EDTDAccrualType AccrualType = EDTDAccrualType::Earned;
```

### enum class EDTDSocialNetwork : uint8

Predefined social media.

Header file:

```cpp
#include "DTDAnalytics/Public/DTDSocialNetwork.h"
```

Values:

* `Facebook = 0`
* `Vkontakte = 1`
* `Twitter = 2`
* `Googleplus = 3`
* `Whatsapp = 4`
* `Viber = 5`
* `Evernote = 6`
* `Googlemail = 7`
* `Linkedin = 8`
* `Pinterest = 9`
* `Reddit = 10`
* `Renren = 11`
* `Tumblr = 12`
* `Qzone = 13`

Example:

```cpp
EDTDSocialNetwork SocialNetwork = EDTDSocialNetwork::Facebook;
```

### enum class EDTDReferralProperty : uint8

Referral properties.

Header file:

```cpp
#include "DTDAnalytics/Public/DTDReferralProperty.h"
```

Values:

* `Source = 0`
* `Medium = 1`
* `Content = 2`
* `Campaign = 3`
* `Term = 4`

Example:

```cpp
EDTDReferralProperty ReferralProperty = EDTDReferralProperty::Source;
```

### struct FDTDOptionalInt32

An optional parameter of int32 type

Header file:

```cpp
#include "DTDAnalytics/Public/DTDOptionalInt32.h"
```

| Member           | Type  | Description     |
| ---------------- | ----- | --------------- |
| ***`HasValue`*** | bool  | Option label    |
| ***`Value`***    | int32 | Parameter value |

For your convenience, we implemented the conversion constructor:

```
FDTDOptionalInt32(int32 value) : HasValue(true), Value(value) {}
```

Example:

```cpp
FDTDOptionalInt32 OptionalParameter = 1;
```

### struct FDTDOptionalString

An optional parameter of FString type.

Header file:

```cpp
#include "DTDAnalytics/Public/DTDOptionalString.h
```

| Member         | Type    | Description     |
| -------------- | ------- | --------------- |
| **`HasValue`** | bool    | Option label    |
| **`Value`**    | FString | Parameter value |

For your convenience, we implemented the conversion constructor:

```cpp
FDTDOptionalString(FString value) : HasValue(true), Value(value) {}
```

Example:

```cpp
FDTDOptionalString OptionalParameter = FString("StringValue");
```

### struct FDTDAnalyticsConfiguration

Configuration of the analytics plugin.

Header file:

```cpp
#include "DTDAnalytics/Public/DTDAnalyticsConfiguration.h"
```

| Member                       | Type               | Description                   |
| ---------------------------- | ------------------ | ----------------------------- |
| ***`LogLevel`***             | EDTDLogLevel       | Logging level                 |
| ***`CurrentLevel`***         | FDTDOptionalInt32  | Current level                 |
| ***`UserId`***               | FDTDOptionalString | User ID                       |
| ***`ApplicationVersion`***   | FDTDOptionalString | Application version (Windows) |
| ***`TrackingAvailability`*** | EDTDTrackingStatus | Tracking settings             |

Example:

```cpp
FDTDAnalyticsConfiguration config;
config.LogLevel = EDTDLogLevel::Debug;
config.CurrentLevel = 3;
config.UserId = FString("CUID");
config.ApplicationVersion = FString("1.2.3");
config.TrackingAvailability = EDTDTrackingStatus::Enable;
```

### FDTDCustomEventParams

Custom parameters of a custom event.

Header file:

```cpp
#include "DTDAnalytics/Public/DTDCustomEventParams.h"
```

| Member                   | Type                    | Description                              |
| ------------------------ | ----------------------- | ---------------------------------------- |
| ***`StringParameters`*** | TMap\<FString, FString> | String parameters                        |
| ***`IntParameters`***    | TMap\<FString, int64>   | Integer parameters                       |
| ***`FloatParameters`***  | TMap\<FString, float>   | Real parameters (floating-point numbers) |
| ***`BoolParameters`***   | TMap\<FString, bool>    | Boolean parameters                       |

{% hint style="info" %}
Warning: avoid duplicating keys in parameters of different types, because in native code dictionaries are merged into a single dictionary \[string: any].
{% endhint %}

Example:

```cpp
FDTDCustomEventParams params;
params.BoolParameters.Add("BoolKey", true);
params.FloatParameters.Add("FloatKey", 3.3);
params.IntParameters.Add("IntKey");
params.StringParameters.Add("StringKey", "StringValue");
```

### struct FDTDStartProgressionEventParams

Parameters of the progression start event.

Header file:

```cpp
#include "DTDAnalytics/Public/DTDStartProgressionEventParams.h"
```

| Member             | Type               | Description |
| ------------------ | ------------------ | ----------- |
| ***`Difficulty`*** | FDTDOptionalInt32  | Difficulty  |
| ***`Source`***     | FDTDOptionalString | Source      |

Example:

```cpp
FDTDStartProgressionEventParams params;
params.Difficulty = 3;
params.Source = FString("Source");
```

### struct FDTDFinishProgressionEventParams

Parameters of the progression completion event.

Header file:

```cpp
#include "DTDAnalytics/Public/DTDFinishProgressionEventParams.h"
```

| Member                       | Type                  | Description                                                   |
| ---------------------------- | --------------------- | ------------------------------------------------------------- |
| ***`SuccessfulCompletion`*** | bool                  | Successful completion of the progression (‘false’ by default) |
| ***`Duration`***             | int32                 | Duration (if 0, duration is calculated automatically)         |
| ***`Spent`***                | TMap\<FString, int64> | Resources spent                                               |
| ***`Earned`***               | TMap\<FString, int64> | Resources earned                                              |

Example:

```cpp
FDTDFinishProgressionEventParams params;
params.Duration = 200;
params.SuccessfulCompletion = true;
params.Earned.Add("CurrencyName1", 1);
params.Spent.Add("CurrencyName2", 2);
```

### Delegates

Header file:

```cpp
#include "DTDAnalytics/Public/DTDDelegates.h"
```

Definitions:

```cpp
DECLARE_DELEGATE_OneParam(FDTDLongListenerDelegate, int64);
DECLARE_DELEGATE_OneParam(FDTDGetterStringDelegate, const FString&);
DECLARE_DELEGATE_OneParam(FDTDGetterBoolDelegate, bool);
DECLARE_DELEGATE_OneParam(FDTDGetterIntDelegate, int32);
DECLARE_DELEGATE_OneParam(FDTDGetterLongDelegate, int64);
DECLARE_DELEGATE_OneParam(FDTDGetterGenderDelegate, EDTDGender);
DECLARE_DELEGATE_TwoParams(FDTDGetterOptionalStringDelegate, bool, const FString&);
DECLARE_DELEGATE_TwoParams(FDTDGetterOptionalBoolDelegate, bool, bool);
DECLARE_DELEGATE_TwoParams(FDTDGetterOptionalFloatDelegate, bool, float);
DECLARE_DELEGATE_TwoParams(FDTDGetterOptionalLongDelegate, bool, int64);
DECLARE_DELEGATE_ThreeParams(FDTDGetterOptionalStringWithKeyDelegate, bool, const FString&, const FString&);
DECLARE_DELEGATE_ThreeParams(FDTDGetterOptionalBoolWithKeyDelegate, bool, const FString&, bool);
DECLARE_DELEGATE_ThreeParams(FDTDGetterOptionalFloatWithKeyDelegate, bool, const FString&, float);
DECLARE_DELEGATE_ThreeParams(FDTDGetterOptionalLongWithKeyDelegate, bool, const FString&, int64);
```

#### enum class EDTDGender: uint8 <mark style="color:red;">(Deprecated)</mark>&#x20;

User gender.

Header file:

```cpp
#include "DTDAnalytics/Public/DTDGender.h"
```

Values:

* `Unknown = 0`
* `Male = 1`
* `Female = 2`

Example:

```cpp
EDTDGender Gender = EDTDGender::Female;
```

## SDK initialization

### SDK initialization without parameters:

![Blueprint](/files/t98ucVqwIV4lFSUx6c8W)

| Member         | Type    | Description                                                                                                                                                                                                                                                          |
| -------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`appKey`*** | FString | <p>You can find it in the settings of the corresponding application in devtodev (Settings → SDK → Integration → Credentials → App ID). <br><br>For <a href="/pages/EBjsmNHf2V14SSZuYNZr">Cross-platform type projects</a> use <code>App ID + Platform ID</code>.</p> |

```cpp
UDTDAnalyticsBPLibrary::Initialize("AppKey");
```

### SDK initialization with parameters:

![Blueprint](/files/i8jo7zoSv0Y7aq6HB5gF)

| Member         | Type                       | Description                                                                                                                                                                                                                                                           |
| -------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`appKey`*** | FString                    | <p>You can find it in the settings of the corresponding application in devtodev (Settings → SDK → Integration → Credentials → App ID).  <br><br>For <a href="/pages/EBjsmNHf2V14SSZuYNZr">Cross-platform type projects</a> use <code>App ID + Platform ID</code>.</p> |
| ***`config`*** | FDTDAnalyticsConfiguration | Initialization parameters                                                                                                                                                                                                                                             |

```cpp
FDTDAnalyticsConfiguration config;
config.LogLevel = EDTDLogLevel::No;
config.CurrentLevel = 3;
config.UserId = FString("CUID");
config.ApplicationVersion = FString("1.2.3");
config.TrackingAvailability = EDTDTrackingStatus::Enable;
UDTDAnalyticsBPLibrary::InitializeWithConfig("App ID", config);
```


# Godot Engine

## Godot SDK integration

The DevToDev SDK extends the Godot engine in a modular way and supports the following platforms: **MacOS, iOS, Android.**

### Requirements <a href="#requirements" id="requirements"></a>

* Godot engine 4.0+ ([Source code](https://docs.godotengine.org/en/4.0/contributing/development/compiling/getting_source.html#doc-getting-source))
* [Python 3.6+](https://www.python.org/downloads/macos/).
* [SCons 3.0+](https://scons.org/pages/download.html) build system.
* SDK module source code ([GitHub repository](https://github.com/devtodev-analytics/Godot-sdk/releases/latest))

### The SDK module installation <a href="#the-sdk-module-installation" id="the-sdk-module-installation"></a>

The SDK module is available in [GitHub repository](https://github.com/devtodev-analytics/Godot-sdk/releases/latest). Download the Source code of latest release and copy ***d2d\_analytics*** folder to the ***modules***(`/godot/modules/`) folder of Godot engine source code.

To work in the Godot editor, compile the engine source code and the analytics module:

```shell
scons platform=macos arch=x86_64
scons platform=macos arch=arm64
lipo -create bin/godot.macos.editor.x86_64 bin/godot.macos.editor.arm64 -output bin/godot.macos.editor.universal

cp -r misc/dist/macos_tools.app ./Godot.app
mkdir -p Godot.app/Contents/MacOS
cp bin/godot.macos.editor.universal Godot.app/Contents/MacOS/Godot
cp modules/d2d_analytics/native/macos/libDTDAnalytics.dylib Godot.app/Contents/MacOS/
chmod +x Godot.app/Contents/MacOS/Godot
codesign --force --timestamp --options=runtime --entitlements misc/dist/macos/editor.entitlements -s - Godot.app
```

[Find out more about compiling for MacOS](https://docs.godotengine.org/en/4.0/contributing/development/compiling/compiling_for_macos.html)

### SDK Initialization <a href="#sdk-initialization" id="sdk-initialization"></a>

For initialization, add the following code at the start of your application:

```gdscript
var config = GDDTDAnalyticsConfiguration.new()
config.logLevel = GDDTDLogLevel.Debug
DTDAnalytics.InitializeWithConfig("App ID", config)
```

You can find the `App ID` in the settings of the respective app in devtodev (Settings → SDK → Integration → [Credentials](/reports-and-functionality/project-related-reports-and-fuctionality/settings#integration)).&#x20;

For [Cross-platform type projects](/getting-started/adding-an-app-to-the-space/cross-platform-application) use `App ID + Platform ID` .

`config` – an object instance of `GDDTDAnalyticsConfiguration`, which is used for specifying additional properties during the initialization

| **Parameter**          | **Type**                   | **Description**                                                                                                                                                                                                                                                                                     |
| ---------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CurrentLevel`         | Integer                    | The player level at the moment of devtodev SDK initialization. It’s optional but we recommend using it for improving data accuracy.                                                                                                                                                                 |
| `UserId`               | String                     | A custom user ID assigned by the developer. In the case of default calculation by device IDs, the identifier can be used for searching users in devtodev. In case the project uses calculation by user IDs, the parameter is mandatory because it becomes the principal calculation ID in devtodev. |
| `TrackingAvailability` | GDDTDTrackingStatus (enum) | The property allows or disallows devtodev tracking of the user. By default, it is set to `GDDTDTrackingStatus.Enable`. SDK stores the previously assigned value. Pass `GDDTDTrackingStatus.Disable` if the user opted out of tracking in line with GDPR.                                            |
| `LogLevel`             | GDDTDLogLevel (enum)       | The level of logging the SDK activity. The `GDDTDLogLevel.No` value is used by default. For troubleshooting during integration it is recommended to set it to `GDDTDLogLevel.Debug`, and either switch it off `GDDTDLogLevel.No`. Use `GDDTDLogLevel.No` in the release version.                    |

\
Example:

```gdscript
var config = GDDTDAnalyticsConfiguration.new() 
config.logLevel = GDDTDLogLevel.No 
config.trackingStatus = GDDTDTrackingStatus.Enable 
config.currentLevel = 1 
config.userId = "unique_userId" 
DTDAnalytics.InitializeWithConfig("AppID", config)
```

## Project export <a href="#project-export" id="project-export"></a>

### Export for MacOS <a href="#export-for-macos" id="export-for-macos"></a>

Open the Export Template Manager to download and install templates:

<figure><img src="/files/jHfo5LIg6uqAnqTvMSSn" alt="" width="298"><figcaption></figcaption></figure>

Open a terminal, go to the root directory of the engine source code. Compile a custom template for MacOS (see [Building export templates](https://docs.godotengine.org/en/4.0/contributing/development/compiling/compiling_for_macos.html#building-export-templates) for MacOS), select Debug or Release build and processor architecture. To support both architectures in a single Universal 2 binary, use ***lipo***:

```sh
scons platform=macos target=template_debug arch=x86_64
scons platform=macos target=template_debug arch=arm64
lipo -create bin/godot.macos.template_debug.x86_64 bin/godot.macos.template_debug.arm64 -output bin/godot.macos.template_debug.universal
```

The next step is to prepare a custom template as a ***macos.zip*** archive. Don't forget to add the DTDAnalytics native library, copy `libDTDAnalytics.dylib` to `macos_template.app/Contents/MacOS/`.

```sh
cp -r misc/dist/macos_template.app .
mkdir -p macos_template.app/Contents/MacOS
cp bin/godot.macos.template_debug.universal macos_template.app/Contents/MacOS/godot_macos_debug.universal
cp modules/d2d_analytics/native/macos/libDTDAnalytics.dylib macos_template.app/Contents/MacOS/
chmod +x macos_template.app/Contents/MacOS/godot_*
chmod +x macos_template.app/Contents/MacOS/libDTDAnalytics.dylib
zip -q -9 -r macos.zip macos_template.app
```

Open the Export menu and specify the path to the prepared custom template:

<figure><img src="/files/sNLLn2R9aDye4mNY9Cxh" alt="" width="563"><figcaption></figcaption></figure>

### Export for iOS <a href="#export-for-ios" id="export-for-ios"></a>

Open the Export Template Manager to download and install templates:

<figure><img src="/files/5DssZRFC6mK5H8Dfj3kR" alt="" width="298"><figcaption></figcaption></figure>

Open a terminal, go to the root directory of the engine source code. And compile a custom template for iOS (see [Compiling](https://docs.godotengine.org/en/4.0/contributing/development/compiling/compiling_for_ios.html#compiling) for iOS). To work with the iOS simulator, compile the sources with the `ios_simulator=yes` flag. To support both architectures in a single Universal 2 binary, use ***lipo***:

```sh
scons p=ios target=template_debug
scons p=ios target=template_release
scons p=ios target=template_debug ios_simulator=yes arch=x86_64
scons p=ios target=template_debug ios_simulator=yes arch=arm64

cp -r misc/dist/ios_xcode .
cp bin/libgodot.ios.template_debug.arm64.a ios_xcode/libgodot.ios.debug.xcframework/ios-arm64/libgodot.a
lipo -create bin/libgodot.ios.template_debug.arm64.simulator.a bin/libgodot.ios.template_debug.x86_64.simulator.a -output ios_xcode/libgodot.ios.debug.xcframework/ios-arm64_x86_64-simulator/libgodot.a

cp bin/libgodot.ios.template_release.arm64.a ios_xcode/libgodot.ios.release.xcframework/ios-arm64/libgodot.a
lipo -create bin/libgodot.ios.template_debug.arm64.simulator.a bin/libgodot.ios.template_debug.x86_64.simulator.a -output ios_xcode/libgodot.ios.release.xcframework/ios-arm64_x86_64-simulator/libgodot.a
```

The next step is to prepare a custom template as an ***ios.zip*** archive. Don't forget to add the DTDAnalytics native library, copy `DTDAnalytics.xcframework` to `ios_xcode/`.

```sh
cp -r modules/d2d_analytics/native/ios/DTDAnalytics.xcframework ios_xcode/
cd "ios_xcode"
zip -q -r ios.zip ./*
cd ..
cp "ios_xcode/ios.zip" ./
```

In XCode project:

1. Add `DTDAnalytics.xcframework` to the project (with ***Do Not Embed*** specified)

   <figure><img src="/files/vei4V2VNjBT6LunnqX3f" alt=""><figcaption></figcaption></figure>
2. Create Bridging-Header. To do this, add any swift file to the project (don't delete it later) and select 'Create Bridging Header' in the dialogue box that appears.<br>

   <figure><img src="/files/NGSNjXXkLwxpHOzY5Rcg" alt="" width="375"><figcaption></figcaption></figure>
3. Add frameworks:
   * `AppTrackingTransparency.framework`
   * `AdSupport.framework`

### Export for Android <a href="#export-for-android" id="export-for-android"></a>

Click to install android templates:

<figure><img src="/files/CdI6iwHHndu7G3nlNNoa" alt="" width="375"><figcaption></figcaption></figure>

After installing the template, an android folder will appear in your project.

Move the `d2d_analytics/native/androidAnalytics.aar` to the `android/plugins/ folder` in your project. You will also need to create an `Analytics.gdap` file in `android/plugins/` with the following content:

```
[config]

name="Analytics"
binary_type="local"
binary="Analytics.aar"

[dependencies]

remote=[
    "com.google.code.gson:gson:2.8.9", 
    "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2", 
    "com.google.android.gms:play-services-ads-identifier:18.0.1",
    "com.devtodev:android-google:1.0.0"
    // Optional (recommended)
    "com.android.installreferrer:installreferrer:2.2"
    ]

custom_maven_repos=["https://repo.maven.apache.org/maven2/"]
```

Next, Analytics should appear in the Plugins section, check it out:

<figure><img src="/files/kG35DzZrtpqbUCl6UenY" alt="" width="563"><figcaption></figcaption></figure>

The next step is to compile the Godot engine for Android (see [Android compilation](https://docs.godotengine.org/en/4.0/contributing/development/compiling/compiling_for_android.html)), choose debug or release build, and select the processor architecture.

```sh
// Example. For debug on arm64v8 architecture use:
scons platform=android target=template_debug arch=arm64v8
//for release:
scons platform=android target=template_release arch=arm64v8
```

After successful compilation execute the following commands:

```sh
// Example. For debug on arm64v8 architecture use:
scons platform=android target=template_debug arch=arm64v8
//for release:
scons platform=android target=template_release arch=arm64v8
```

In the next step in `godot-4.0-stable/bin` you will see `godot-lib.template_debug.aar` or

`godot-lib.template_relaese.aar`.

You need to copy and replace this file to the previously installed template in your project at the path:

* `appName/android/build/libs/debug` - for debugging
* `appName/android/build/libs/release` - for release

After these steps, you are ready to export your Android app.

<figure><img src="/files/DbINrif6mMhm2qWBWR1M" alt="" width="563"><figcaption></figcaption></figure>

Make sure that `Use Gradle Build` (in the Gradle Build section), `Analytics` (in the Plugins section) and the previously compiled `Architecture` (in the Architectures section) are selected.


# Setting up Events

{% content-ref url="/pages/-MhDpnkL0zK1J7s6x-oK" %}
[Basic methods](/integration/integration-of-sdk-v2/setting-up-events/basic-methods)
{% endcontent-ref %}

{% content-ref url="/pages/-MhDprH27Nb11KM0omop" %}
[Secondary methods](/integration/integration-of-sdk-v2/setting-up-events/secondary-methods)
{% endcontent-ref %}

{% content-ref url="/pages/-MhDpwXXuGNEV5GixSHe" %}
[User profile](/integration/integration-of-sdk-v2/setting-up-events/user-profile)
{% endcontent-ref %}

{% content-ref url="/pages/-MhDshyfvOboO0jcGGte" %}
[Anticheat methods](/integration/integration-of-sdk-v2/setting-up-events/anticheat-methods)
{% endcontent-ref %}

{% content-ref url="/pages/wBJVPZtJVBnBTJ5C1Dtg" %}
[Track sessions](/integration/integration-of-sdk-v2/setting-up-events/track-sessions)
{% endcontent-ref %}


# Basic methods

Please take a look at our [Expert tips](/integration/expert-tips/what-to-track) before integrating the events.

## Real Payment

To track payments in a real currency, dispatch this event right after the system validates that the payment went through successfully. The event is fundamental and mandatory for all the app metrics related to monetization.

{% hint style="warning" %}
For purchases made through the App Store and Google Play Market, [automatic data collection](/integration/autocapture/automatic-payment-tracking) for in-app payments is available starting from SDK version **2.5.0** and higher. To enable automatic data collection, provide valid credentials in the settings section under **Payments integration → IAP auto tracking**. Once the credentials are successfully verified, all new purchases made in your application will be automatically sent as a **Real Payment** event (with the source specified as auto).

We recommend avoiding simultaneous event sending through automatic tracking and manual submission of the basic **Real Payment** event, as this may lead to data duplication.
{% endhint %}

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
DTDAnalytics.realCurrencyPayment(orderId: "Order ID", 
                                 price: 12.5, 
                                 productId: "Product ID", 
                                 currencyCode: "USD")
```

<table><thead><tr><th width="139">Parameter</th><th width="96">Type</th><th width="256">Restrictions</th><th>Description</th></tr></thead><tbody><tr><td><em><strong><code>orderId</code></strong></em></td><td>string</td><td>from 1 to 65 symbols</td><td>A unique transaction ID. Use <strong><code>transactionIdentifier</code></strong> property value in <strong><code>SKPaymentTransaction</code></strong> object in a complete transaction receipt.</td></tr><tr><td><em><strong><code>currencyCode</code></strong></em></td><td>string</td><td>precisely 3 symbols</td><td>Transaction currency (<a href="https://en.wikipedia.org/wiki/ISO_4217">ISO 4217 standard</a>) e.g. <em><strong>USD, EUR</strong></em> etc.</td></tr><tr><td><em><strong><code>price</code></strong></em></td><td>double</td><td>from Double.min to Double.max</td><td>The item price in the transaction currency.</td></tr><tr><td><em><strong><code>productId</code></strong></em></td><td>string</td><td>from 1 to 255 symbols</td><td>Item name. We recommend using a bundle or names in the same language.</td></tr></tbody></table>
{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

```swift
[DTDAnalytics realCurrencyPaymentWithOrderId:@"Order ID"
                                       price:12.5
                                   productId:@"Product ID"
                                currencyCode:@"USD"];
```

<table><thead><tr><th width="153.5">Parameter</th><th width="133">Type</th><th width="261">Restrictions</th><th>Description</th></tr></thead><tbody><tr><td><em><strong><code>orderId</code></strong></em></td><td>NSString</td><td>from 1 to 65 symbols</td><td>A unique transaction ID. Use <strong><code>transactionIdentifier</code></strong> property value in <strong><code>SKPaymentTransaction</code></strong> object in a complete transaction receipt.</td></tr><tr><td><em><strong><code>currencyCode</code></strong></em></td><td>NSString</td><td>precisely 3 symbols</td><td>Transaction currency (<a href="https://en.wikipedia.org/wiki/ISO_4217">ISO 4217 standard</a>) e.g. <em><strong>USD, EUR</strong></em> etc.</td></tr><tr><td><em><strong><code>price</code></strong></em></td><td>double</td><td>from Double.min to Double.max</td><td>The item price in the transaction currency.</td></tr><tr><td><em><strong><code>productId</code></strong></em></td><td>NSString</td><td>from 1 to 255 symbols</td><td>Item name. We recommend using a bundle or names in the same language.</td></tr></tbody></table>
{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
DTDAnalytics.realCurrencyPayment(
    orderId = "Order ID",
    price = 12.5,
    productId = "Product ID",
    currencyCode = "USD"
)
```

<table><thead><tr><th width="139.5">Parameter</th><th width="99">Type</th><th width="281">Restrictions</th><th>Description</th></tr></thead><tbody><tr><td><em><strong><code>orderId</code></strong></em></td><td>string</td><td>from 1 to 65 symbols</td><td>A unique transaction ID. </td></tr><tr><td><em><strong><code>currencyCode</code></strong></em></td><td>string</td><td>precisely 3 symbols</td><td>Transaction currency (<a href="https://en.wikipedia.org/wiki/ISO_4217">ISO 4217 standard</a>) e.g. <em><strong>USD, EUR</strong></em> etc.</td></tr><tr><td><em><strong><code>price</code></strong></em></td><td>double</td><td>from Double.min to Double.max</td><td>The item price in the transaction currency.</td></tr><tr><td><em><strong><code>productId</code></strong></em></td><td>string</td><td>from 1 to 255 symbols</td><td>Item name. We recommend using a bundle or names in the same language.</td></tr></tbody></table>

{% hint style="info" %}
How to find the transaction ID in GooglePlay transaction?

Find the INAPP\_PURCHASE\_DATA object In the JSON fields that are returned in the response data for a purchase order. A unique transaction identifier is the value of orderId property in INAPP\_PURCHASE\_DATA object. If the order is a test purchase made via the In-app Billing Sandbox, orderId property will be empty.
{% endhint %}
{% endtab %}

{% tab title="Android (Java)" %}

```java
 DTDAnalytics.INSTANCE.realCurrencyPayment(
        "Order ID",
        12.5,
        "Product ID",
        "USD"
);
```

<table><thead><tr><th width="145.5">Parameter</th><th width="101">Type</th><th width="262">Restrictions</th><th>Description</th></tr></thead><tbody><tr><td><em><strong><code>orderId</code></strong></em></td><td>string</td><td>from 1 to 65 symbols</td><td>A unique transaction ID. </td></tr><tr><td><em><strong><code>currencyCode</code></strong></em></td><td>string</td><td>precisely 3 symbols</td><td>Transaction currency (<a href="https://en.wikipedia.org/wiki/ISO_4217">ISO 4217 standard</a>) e.g. <em><strong>USD, EUR</strong></em> etc.</td></tr><tr><td><em><strong><code>price</code></strong></em></td><td>double</td><td>from Double.min to Double.max</td><td>The item price in the transaction currency.</td></tr><tr><td><em><strong><code>productId</code></strong></em></td><td>string</td><td>from 1 to 255 symbols</td><td>Item name. We recommend using a bundle or names in the same language.</td></tr></tbody></table>

{% hint style="info" %}
How to find the transaction ID in GooglePlay transaction?

Find the INAPP\_PURCHASE\_DATA object In the JSON fields that are returned in the response data for a purchase order. A unique transaction identifier is the value of orderId property in INAPP\_PURCHASE\_DATA object. If the order is a test purchase made via the In-app Billing Sandbox, orderId property will be empty.
{% endhint %}
{% endtab %}

{% tab title=".NET Native + UWP" %}

```csharp
DTDAnalytics.RealCurrencyPayment(
    orderId: "Order ID",
    price: 12.5,
    productId: "Product ID",
    currencyCode: "USD");
```

<table><thead><tr><th width="141.5">Parameter</th><th width="81">Type</th><th width="336">Restrictions</th><th>Description</th></tr></thead><tbody><tr><td><em><strong><code>orderId</code></strong></em></td><td>string</td><td>from 1 to 65 symbols</td><td>A unique transaction ID.</td></tr><tr><td><em><strong><code>currencyCode</code></strong></em></td><td>string</td><td>precisely 3 symbols</td><td>Transaction currency (<a href="https://en.wikipedia.org/wiki/ISO_4217">ISO 4217 standard</a>) e.g. <em><strong>USD, EUR</strong></em> etc.</td></tr><tr><td><em><strong><code>price</code></strong></em></td><td>double</td><td>from double.MinValue to double.MaxValue</td><td>The item price in the transaction currency.</td></tr><tr><td><em><strong><code>productId</code></strong></em></td><td>string</td><td>from 1 to 255 symbols</td><td>Item name. We recommend using a bundle or names in the same language.</td></tr></tbody></table>
{% endtab %}

{% tab title="Unity" %}

```csharp
DTDAnalytics.RealCurrencyPayment(
    orderId: "Order ID",
    price: 12.5,
    productId: "Product ID",
    currencyCode: "USD");
```

<table><thead><tr><th width="161.5">Parameter</th><th width="115">Type</th><th width="347">Restrictions</th><th>Description</th></tr></thead><tbody><tr><td><em><strong><code>orderId</code></strong></em></td><td>string</td><td>from 1 to 65 symbols</td><td>A unique transaction ID.</td></tr><tr><td><em><strong><code>currencyCode</code></strong></em></td><td>string</td><td>precisely 3 symbols</td><td>Transaction currency (<a href="https://en.wikipedia.org/wiki/ISO_4217">ISO 4217 standard</a>) e.g. <em><strong>USD, EUR</strong></em> etc.</td></tr><tr><td><em><strong><code>price</code></strong></em></td><td>double</td><td>from Double.MinValue to Double.MaxValue</td><td>The item price in the transaction currency.</td></tr><tr><td><em><strong><code>productId</code></strong></em></td><td>string</td><td>from 1 to 255 symbols</td><td>Item name. We recommend using a bundle or names in the same language.</td></tr></tbody></table>
{% endtab %}

{% tab title="Web" %}

```javascript
analytics.realCurrencyPayment(
                                orderId,
                                price,
                                productId,
                                currencyCode)
```

<table><thead><tr><th width="143.5">Parameter</th><th width="101">Type</th><th width="244">Restrictions</th><th>Description</th></tr></thead><tbody><tr><td><em><strong><code>orderId</code></strong></em></td><td>string</td><td>from 1 to 65 symbols</td><td>A unique transaction ID.</td></tr><tr><td><em><strong><code>currencyCode</code></strong></em></td><td>string</td><td>precisely 3 symbols</td><td>Transaction currency (<a href="https://en.wikipedia.org/wiki/ISO_4217">ISO 4217 standard</a>) e.g. <em><strong>USD, EUR</strong></em> etc.</td></tr><tr><td><em><strong><code>price</code></strong></em></td><td>double</td><td>from Number.MIN_VALUE to Number.MAX_VALUE</td><td>The item price in the transaction currency.</td></tr><tr><td><em><strong><code>productId</code></strong></em></td><td>string</td><td>from 1 to 255 symbols</td><td>Item name. We recommend using a bundle or names in the same language.</td></tr></tbody></table>
{% endtab %}

{% tab title="Unreal" %}
![Blueprint](/files/k3PeVCUDx3yWPX0W9lET)

<table><thead><tr><th width="157.5">Parameter</th><th width="96">Type</th><th width="315">Restrictions</th><th>Description</th></tr></thead><tbody><tr><td><em><strong><code>orderId</code></strong></em></td><td>FString</td><td>from 1 to 65 symbols</td><td>A unique transaction ID.</td></tr><tr><td><em><strong><code>currencyCode</code></strong></em></td><td>FString</td><td>precisely 3 symbols</td><td>Transaction currency (<a href="https://en.wikipedia.org/wiki/ISO_4217">ISO 4217 standard</a>) e.g. <strong>USD</strong>, <strong>EUR</strong> etc.</td></tr><tr><td><em><strong><code>price</code></strong></em></td><td>float</td><td>from float.MinValue to float.MaxValue</td><td>The item price in the transaction currency.</td></tr><tr><td><em><strong><code>productId</code></strong></em></td><td>FString</td><td>from 1 to 255 symbols</td><td>Item name. We recommend using a bundle or names in the same language.</td></tr></tbody></table>

```cpp
1UDTDAnalyticsBPLibrary::RealCurrencyPayment("OrderId", 12.5, "ProductId", "USD");
```

{% endtab %}

{% tab title="Godot" %}

```swift
DTDAnalytics.RealCurrencyPayment("orderId", 9.99, "productId", "USD")
```

<table><thead><tr><th width="140.5">Parameter</th><th width="114">Type</th><th width="316">Restrictions</th><th>Description</th></tr></thead><tbody><tr><td><em><strong><code>orderId</code></strong></em></td><td>String</td><td>from 1 to 65 symbols</td><td>A unique transaction ID.</td></tr><tr><td><em><strong><code>currencyCode</code></strong></em></td><td>String</td><td>precisely 3 symbols</td><td>Transaction currency (<a href="https://en.wikipedia.org/wiki/ISO_4217">ISO 4217 standard</a>) e.g. <em><strong>USD, EUR</strong></em> etc.</td></tr><tr><td><em><strong><code>price</code></strong></em></td><td>Float</td><td>from Double.min to Double.max</td><td>The item price in the transaction currency.</td></tr><tr><td><em><strong><code>productId</code></strong></em></td><td>String</td><td>from 1 to 255 symbols</td><td>Item name. We recommend using a bundle or names in the same language.</td></tr></tbody></table>
{% endtab %}
{% endtabs %}

{% hint style="info" %}
By default (easy to change in the app’s settings) devtodev server invalidates transactions with previously-used identifiers. Besides, the server performs identifier checks by its outer appearance in order to avoid obvious fraud.

If you want to exclude fraud payments from your reports altogether, before creating ‘Real Currency Payment’ event, use devtodev anti-cheat feature.
{% endhint %}

## Custom Events

If you want to track non-basic events, you can create custom events of your own. How you are going to apply them depends solely on you.

{% hint style="warning" %}
Attention! We strongly recommend that you do not use custom event properties to transfer and store data that fits the definition of [personal data](https://gdpr-info.eu/issues/personal-data/)!
{% endhint %}

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
DTDAnalytics.customEvent(eventName: "Event name")
```

If you want to pass custom parameters, use **`DTDCustomEventParameters`** class instance.

```swift
let parameters = DTDCustomEventParameters()
parameters.add(key: "key for string value", value: "string value")
parameters.add(key: "key for int value", value: 10)
parameters.add(key: "key for bool value", value: true)
parameters.add(key: "key for double value", value: 12.5)

DTDAnalytics.customEvent(eventName: "Event name", parameters: parameters)
```

| Parameter          | Type                     | Restrictions                                              | Description              |
| ------------------ | ------------------------ | --------------------------------------------------------- | ------------------------ |
| ***`eventName`***  | string                   | from 1 to 72 symbols                                      | Custom event name.       |
| ***`parameters`*** | DTDCustomEventParameters | <p>key - from 1 to 32 symbols</p><p>value - see below</p> | Custom event parameters. |

The following data types can be passed using the **`DTDCustomEventParameters`** object:

| Type         | Restrictions                  |
| ------------ | ----------------------------- |
| **`int`**    | from Int64.min to Int64.max   |
| **`string`** | from 1 to 255 symbols         |
| **`bool`**   | true/false                    |
| **`double`** | from Double.min to Double.max |
| {% endtab %} |                               |

{% tab title="iOS+macOS (Objective-C)" %}

```objectivec
[DTDAnalytics customEvent:@"Event name"];
```

If you want to pass custom parameters, use **`DTDCustomEventParameters`** class instance.

```objectivec
DTDCustomEventParameters *parameters = [[DTDCustomEventParameters alloc] init];
[parameters addString:@"key for string value" value:@"string value"];
[parameters addInt:@"key for int value" value:10];
[parameters addBool:@"key for bool value" value:true];
[parameters addDouble:@"key for double value" value:12.5];

[DTDAnalytics customEvent:@"Event name" withParameters:parameters];
```

| Parameter          | Type                     | Restrictions                                              | Description              |
| ------------------ | ------------------------ | --------------------------------------------------------- | ------------------------ |
| ***`eventName`***  | NSString                 | from 1 to 72 symbols                                      | Custom event name.       |
| ***`parameters`*** | DTDCustomEventParameters | <p>key - from 1 to 32 symbols</p><p>value - see below</p> | Custom event parameters. |

The following data types can be passed using the **`DTDCustomEventParameters`** object:

| Type         | Restrictions                  |
| ------------ | ----------------------------- |
| **`int`**    | from Int64.min to Int64.max   |
| **`string`** | from 1 to 255 symbols         |
| **`bool`**   | true/false                    |
| **`double`** | from Double.min to Double.max |
| {% endtab %} |                               |

{% tab title="Android (Kotlin)" %}

```kotlin
DTDAnalytics.customEvent(eventName = "Event name")
```

If you want to pass custom parameters, use **`DTDCustomEventParameters`** class instance.

```java
let parameters = DTDCustomEventParameters()
parameters.add(key = "key for string value", value = "string value")
parameters.add(key = "key for int value", value = 10)
parameters.add(key = "key for bool value", value = true)
parameters.add(key = "key for double value", value = 12.5)

DTDAnalytics.customEvent(
    eventName = "Event name", 
    customEventParameters = parameters
)
```

| Parameter          | Type                     | Restrictions                                              | Description              |
| ------------------ | ------------------------ | --------------------------------------------------------- | ------------------------ |
| ***`eventName`***  | string                   | from 1 to 72 symbols                                      | Custom event name.       |
| ***`parameters`*** | DTDCustomEventParameters | <p>key - from 1 to 32 symbols</p><p>value - see below</p> | Custom event parameters. |

The following data types can be passed using the **`DTDCustomEventParameters`** object:

| Type          | Restrictions                  |
| ------------- | ----------------------------- |
| **`Long`**    | from Long.min to Long.max     |
| **`String`**  | from 1 to 255 symbols         |
| **`Boolean`** | true/false                    |
| **`Double`**  | from Double.min to Double.max |
| {% endtab %}  |                               |

{% tab title="Android (Java)" %}

```java
DTDAnalytics.INSTANCE.customEvent("Event name");
```

If you want to pass custom parameters, use **`DTDCustomEventParameters`** class instance.

```java
DTDCustomEventParameters parameters = new DTDCustomEventParameters();
parameters.add("key for string value", "string value");
parameters.add("key for int value", 10);
parameters.add("key for bool value", true);
parameters.add("key for double value", 12.5);
DTDAnalytics.INSTANCE.customEvent("Event name", parameters);
```

| Parameter          | Type                     | Restrictions                                              | Description              |
| ------------------ | ------------------------ | --------------------------------------------------------- | ------------------------ |
| ***`eventName`***  | string                   | from 1 to 72 symbols                                      | Custom event name.       |
| ***`parameters`*** | DTDCustomEventParameters | <p>key - from 1 to 32 symbols</p><p>value - see below</p> | Custom event parameters. |

The following data types can be passed using the **`DTDCustomEventParameters`** object:

| Type          | Restrictions                  |
| ------------- | ----------------------------- |
| **`Long`**    | from Long.min to Long.max     |
| **`String`**  | from 1 to 255 symbols         |
| **`Boolean`** | true/false                    |
| **`Double`**  | from Double.min to Double.max |
| {% endtab %}  |                               |

{% tab title=".NET Native + UWP" %}

```csharp
DTDAnalytics.CustomEvent(eventName: "Event name");
```

If you want to pass custom parameters, use **`DTDCustomEventParameters`** class instance.

```csharp
var parameters = new DTDCustomEventParameters();
parameters.Add(key: "key for string value", value: "string value");
parameters.Add(key: "key for int value", value: 10);
parameters.Add(key: "key for bool value", value: true);
parameters.Add(key: "key for double value", value: 12.5);
DTDAnalytics.CustomEvent(eventName: "Event name", parameters: parameters);
```

| Parameter          | Type                     | Restrictions                                              | Description              |
| ------------------ | ------------------------ | --------------------------------------------------------- | ------------------------ |
| ***`eventName`***  | string                   | from 1 to 72 symbols                                      | Custom event name.       |
| ***`parameters`*** | DTDCustomEventParameters | <p>key - from 1 to 32 symbols</p><p>value - see below</p> | Custom event parameters. |

The following data types can be passed using the **`DTDCustomEventParameters`** object:

| Type         | Restrictions                            |
| ------------ | --------------------------------------- |
| **`long`**   | from long.MinValue to long.MaxValue     |
| **`string`** | from 1 to 255 symbols                   |
| **`bool`**   | true/false                              |
| **`double`** | from double.MinValue to double.MaxValue |
| {% endtab %} |                                         |

{% tab title="Unity" %}

```csharp
DTDAnalytics.CustomEvent(eventName: "Event name");
```

If you want to pass custom parameters, use **`DTDCustomEventParameters`** class instance.

```csharp
var parameters = new DTDCustomEventParameters();
parameters.Add(key: "key for string value", value: "string value");
parameters.Add(key: "key for int value", value: 10);
parameters.Add(key: "key for bool value", value: true);
parameters.Add(key: "key for double value", value: 12.5);
DTDAnalytics.CustomEvent(eventName: "Event name", parameters: parameters);
```

| Parameter          | Type                     | Restrictions                                              | Description              |
| ------------------ | ------------------------ | --------------------------------------------------------- | ------------------------ |
| ***`eventName`***  | string                   | from 1 to 72 symbols                                      | Custom event name.       |
| ***`parameters`*** | DTDCustomEventParameters | <p>key - from 1 to 32 symbols</p><p>value - see below</p> | Custom event parameters. |

The following data types can be passed using the **`DTDCustomEventParameters`** object:

| Type         | Restrictions                            |
| ------------ | --------------------------------------- |
| **`long`**   | from Int64.MinValue to Int64.MaxValue   |
| **`string`** | from 1 to 255 symbols                   |
| **`bool`**   | true/false                              |
| **`double`** | from Double.MinValue to Double.MaxValue |
| {% endtab %} |                                         |

{% tab title="Web" %}

```javascript
analytics.customEvent(eventName, parameters)
```

If you want to pass custom parameters, use an object with parameters.

```javascript
analytics.customEvent("Event name", {
        "key for string value" : "string value",
        "key for int value": 10,
        "key for bool value": true,
        "key for double value": 12.5
})
```

| Parameter          | Type   | Restrictions                                              | Description              |
| ------------------ | ------ | --------------------------------------------------------- | ------------------------ |
| ***`eventName`***  | string | from 1 to 72 symbols                                      | Custom event name.       |
| ***`parameters`*** | object | <p>key - from 1 to 32 symbols</p><p>value - see below</p> | Custom event parameters. |

The following data types can be passed using parameters object:

| Type         | Restrictions                                                |
| ------------ | ----------------------------------------------------------- |
| **`long`**   | from Number.MIN\_SAFE\_INTEGER to Number.MAX\_SAFE\_INTEGER |
| **`string`** | from 1 to 255 symbols                                       |
| **`bool`**   | true/false                                                  |
| **`double`** | from Number.MIN\_VALUE to Number.MAX\_VALUE                 |
| {% endtab %} |                                                             |

{% tab title="Unreal" %}
![Blueprint](/files/6Jri0ivYGRGn19rl2HqI)

| Parameter         | Type    | Restrictions         | Description        |
| ----------------- | ------- | -------------------- | ------------------ |
| ***`eventName`*** | FString | from 1 to 72 symbols | Custom event name. |

```cpp
UDTDAnalyticsBPLibrary::CustomEvent("EventName");
```

If you want to pass custom parameters, use:

![Blueprint](/files/WFU0Zbd1XNTv8RAODrpH)

| Parameter          | Type                  | Restrictions                                                                                                                                                                                                                                                      | Description                                                                                                                                                                                          |
| ------------------ | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`eventName`***  | FString               | from 1 to 72 symbols                                                                                                                                                                                                                                              | Custom event name.                                                                                                                                                                                   |
| ***`parameters`*** | FDTDCustomEventParams | <ul><li>StringParameters (TMap\<FString, FString>)</li><li>IntParameters (TMap\<FString, int64>)</li><li>FloatParameters (TMap\<FString, float>)</li><li>BoolParameters (TMap\<FString, bool>)</li></ul><p>key - from 1 to 32 symbols</p><p>value - see below</p> | <p>Custom event parameters.</p><p><strong>Warning:</strong> avoid duplicate keys in all dictionaries. Because dictionaries are combined into a generic dictionary \[string: any] in native code.</p> |

The following data types can be passed using the **DTDCustomEventParameters** object:

| Type    | Restrictions                          |
| ------- | ------------------------------------- |
| int64   | from int64.MinValue to int64.MaxValue |
| FString | from 1 to 255 symbols                 |
| bool    | true/false                            |
| float   | from float.MinValue to float.MaxValue |

```cpp
FDTDCustomEventParams params;
params.BoolParameters.Add("BoolKey", true);
params.FloatParameters.Add("FloatKey", 3.3);
params.IntParameters.Add("IntKey");
params.StringParameters.Add("StringKey", "StringValue");
UDTDAnalyticsBPLibrary::CustomEventWithParams("EventName", params);
```

{% endtab %}

{% tab title="Godot" %}

```gdscript
DTDAnalytics.CustomEvent("CustomEvent")
```

If you want to pass custom parameters, use **`GDDTDCustomEventParameters`** class instance.

```gdscript
var parameters = GDDTDCustomEventParams.new()
parameters.AddStringValue("str_key", "str_value")
parameters.AddBoolValue("bool_key", true)
parameters.AddIntegerValue("int_key", 100)
parameters.AddFloatValue("float_key", 0.0015)
DTDAnalytics.CustomEventWithParams("CustomEventWithParams", parameters)
```

| Parameter          | Type                   | Restrictions                                              | Description              |
| ------------------ | ---------------------- | --------------------------------------------------------- | ------------------------ |
| ***`eventName`***  | string                 | from 1 to 72 symbols                                      | Custom event name.       |
| ***`parameters`*** | GDDTDCustomEventParams | <p>key - from 1 to 32 symbols</p><p>value - see below</p> | Custom event parameters. |

The following data types can be passed using the **`GDDTDCustomEventParameters`** object:

| Type          | Restrictions                |
| ------------- | --------------------------- |
| **`int`**     | from Int64.min to Int64.max |
| **`String`**  | from 1 to 255 symbols       |
| **`bool`**    | true/false                  |
| **`Float`**   | from Float.min to Float.max |
| {% endtab %}  |                             |
| {% endtabs %} |                             |

{% hint style="warning" %}
devtodev supports no more than 300 custom event names in a single project (see [Limits](/data-management-and-limits#data-limits)). Events that exceed the limit of custom event names will be discarded. Try to integrate the tracked actions by type to the event name level, and move the characteristic tags to the parameters.

*For example, if you need to track purchasing “Paper” and “Pen” items, then you don’t need to create two events with the names “Paper Purchase” and “Pen Purchase”. Create a “Purchase” event and add an “Item” parameter to it with the appropriate “Paper” or “Pen” value. This way, you can use just one event to track many items.*

For a string parameter, you can use no more than 50,000 unique values ​​for the entire history of events. If the number of unique values exceeds the limit, the parameter gets locked by the system and is discarded from the received data. Therefore, we don’t recommend using highly variable parameters like user IDs or time as string values ​​(moreover, they are automatically added to the event).

We strongly recommend that you do not change the data type passed in the same parameter. If you change the data type in a parameter, it will be duplicated with the same name, which may cause issues while processing reports.
{% endhint %}

## Subscriptions

{% hint style="warning" %}
The described method is available beginning with version 2.1.0!

Tracking of subscriptions is now available for Apple App Store and Google Play only.
{% endhint %}

{% tabs fullWidth="true" %}
{% tab title="App Store (Swift)" %}
{% hint style="info" %}
Please note that in order to track subscriptions, you need to do the following:<br>

1. Call the **`subscriptionPayment`** method (described below)
2. [Configure data transfer from the App Store Connect](https://docs.devtodev.com/integration/integration-of-sdk-v2/setting-up-events/pages/-MeeHqlQ6GNgcFpPS9uN#step-1.-settings-on-app-store-connect-side)
3. [Configure integration in the devtodev application settings](https://docs.devtodev.com/integration/integration-of-sdk-v2/setting-up-events/pages/-MeeHqlQ6GNgcFpPS9uN#step-2.-settings-on-devtodev-side)
   {% endhint %}

To track your income from subscriptions, you need to call the following method at the moment of the subscription purchase even if the user signed up for a trial subscription: **`func subscriptionPayment(transaction: SKPaymentTransaction, product: SKProduct).`**

For example:

```swift
extension Purchases: SKPaymentTransactionObserver {
  func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    for transaction in transactions { 
      switch transaction.transactionState {
      case .purchased:
          // Your code ...
          if let product = products?[transaction.payment.productIdentifier] {
             DTDAnalytics.subscriptionPayment(transaction: transaction, product: product)
          }
      case .restored:
          // Your code ...
      case .failed:
          // Your code ...
      default:
          // Your code ...
      }
    }
  }
}
```

Further user actions - renewal, unsubscription, etc. are tracked by using the data received from AppStore in the server-server format. You will need the corresponding setting for it.

Also, if you want to track changes in the status of the subscriptions purchased before devtodev SDK 2.0 integration, you need to transfer your history of previously purchased subscriptions to devtodev.

The SDK monitors the need for historical data to avoid sending out excessive queries to App Store. Use the **`DTDAnalytics.isRestoreTransactionHistoryRequired`**&#x6D;ethod to check whether or not there is a need in sending out the information about the previously purchased subscriptions to devtodev. The method returns BOOL value.

An example of a purchase history query with verification of the need for it:

```swift
DTDAnalytics.isRestoreTransactionHistoryRequired { isNeedRestore in
  if isNeedRestore {
    DispatchQueue.main.async {
      SKPaymentQueue.default().restoreCompletedTransactions()
    }
  }
}
```

Use the **`DTDAnalytics.subscriptionHistory`** method to transfer the list of previously purchased subscriptions received from App Store.&#x20;

{% hint style="warning" %}
If your project accounts users by user ID (not by device ID) and the device is used by more than one user, you need to filter the transaction history so that it will contain only those transactions that belong to the active user. Otherwise, subscriptions of all device users will be attributed to the user who was the first to launch the app after the integration of subscription tracking.
{% endhint %}

```swift
extension Purchases: SKPaymentTransactionObserver {
  func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
    // Your code ...
    let restoredTransactions = queue.transactions.filter { $0.transactionState == .restored }
    DTDAnalytics.subscriptionHistory(transactions: restoredTransactions)
  }
}
```

{% hint style="info" %}
To recover the purchase history, the user should be logged in with his Apple ID. Be mindful of this before starting the recovering process.
{% endhint %}
{% endtab %}

{% tab title="App Store (Swift) + StoreKit 2" %}
{% hint style="info" %}
Please note that in order to track subscriptions, you need to do the following:<br>

1. Call the **`subscriptionPayment`** method (described below)
2. [Configure data transfer from the App Store Connect](https://docs.devtodev.com/integration/integration-of-sdk-v2/setting-up-events/pages/-MeeHqlQ6GNgcFpPS9uN#step-1.-settings-on-app-store-connect-side)
3. [Configure integration in the devtodev application settings](https://docs.devtodev.com/integration/integration-of-sdk-v2/setting-up-events/pages/-MeeHqlQ6GNgcFpPS9uN#step-2.-settings-on-devtodev-side)
   {% endhint %}

To track your income from subscriptions, you need to call the following method at the moment of the subscription purchase even if the user signed up for a trial subscription: **`func subscriptionPayment(transaction: Transaction, product: Product).`**

For example:

```swift
func purchase(_ product: Product) async throws {
  let result = try await product.purchase()

  switch result {
  case let .success(.verified(transaction)):
    // Successful purchase
    await transaction.finish()
    DTDAnalytics.subscriptionPayment(transaction: transaction, product: product)

  case let .success(.unverified(_, error)):
    // Successful purchase but transaction/receipt can't be verified
    // Could be a jailbroken phone
    print(error)

  case .pending:
    // Transaction waiting on SCA (Strong Customer Authentication) or
    // approval from Ask to Buy
    break
  case .userCancelled:
    // Do nothing
    break

  @unknown default:
    break
  }
}
```

Fork with **Transaction**.updates:

```swift
private func listenForTransactions() -> Task<Void, Error> {
  return Task.detached {
      // Iterate through any transactions that don't come from a direct call to `purchase()`.
      for await verificationResult in Transaction.updates {
      guard let case .verified(let transaction) = verificationResult else { return }
      if let revocationDate = transaction.revocationDate {
        // Remove access to the product identified by transaction.productID.
        // Transaction.revocationReason provides details about
        // the revoked transaction.
      } else if let expirationDate = transaction.expirationDate,
                    expirationDate < Date() {
        // Do nothing, this subscription is expired.
        return
      } else if transaction.isUpgraded {
        // Do nothing, there is an active transaction
       // for a higher level of service.
       return
      } else {
       // Provide access to the product identified by
       // transaction.productID.
        if let product = self.products.first(where: { $0.id == transaction.productID }) {
          DTDAnalytics.subscriptionPayment(transaction: transaction, product: product)
        }
      }
    }
  }
}
```

Further user actions - renewal, unsubscription, etc. are tracked by using the data received from AppStore in the server-server format. You will need the corresponding setting for it.

Also, if you want to track changes in the status of the subscriptions purchased before devtodev SDK 2.0 integration, you need to transfer your history of previously purchased subscriptions to devtodev.

The SDK monitors the need for historical data to avoid sending out excessive queries to App Store. Use the **`DTDAnalytics.isRestoreTransactionHistoryRequired`**&#x6D;ethod to check whether or not there is a need in sending out the information about the previously purchased subscriptions to devtodev. The method returns BOOL value.

An example of a purchase history query with verification of the need for it:

```swift
DTDAnalytics.isRestoreTransactionHistoryRequired { [weak self] flag in
  if flag {
    Task {
      await self?.restoreTransactions()
    }
  }
}
```

Use the **`DTDAnalytics.subscriptionHistory`** method to transfer the list of previously purchased subscriptions received from App Store.&#x20;

{% hint style="warning" %}
If your project accounts users by user ID (not by device ID) and the device is used by more than one user, you need to filter the transaction history so that it will contain only those transactions that belong to the active user. Otherwise, subscriptions of all device users will be attributed to the user who was the first to launch the app after the integration of subscription tracking.
{% endhint %}

```swift
extension Purchases: SKPaymentTransactionObserver {
func restoreTransactions() async {
  var transactions: [Transaction] = []
  for await transaction in Transaction.all {
    if case let .verified(verifiedTransaction) = transaction {
      transactions.append(verifiedTransaction)
    }
  }

  DTDAnalytics.subscriptionHistory(transactions: transactions)
}
```

{% hint style="info" %}
To recover the purchase history, the user should be logged in with his Apple ID. Be mindful of this before starting the recovering process.
{% endhint %}
{% endtab %}

{% tab title="App Store (Objective-C) " %}
{% hint style="info" %}
Please note that in order to track subscriptions, you need to do the following:<br>

1. Call the **`subscriptionPayment`** method (described below)
2. [Configure data transfer from the App Store Connect](https://docs.devtodev.com/integration/integration-of-sdk-v2/setting-up-events/pages/-MeeHqlQ6GNgcFpPS9uN#step-1.-settings-on-app-store-connect-side)
3. [Configure integration in the devtodev application settings](https://docs.devtodev.com/integration/integration-of-sdk-v2/setting-up-events/pages/-MeeHqlQ6GNgcFpPS9uN#step-2.-settings-on-devtodev-side)
   {% endhint %}

To track your income from subscriptions, you need to call the following method at the moment of the subscription purchase even if the user signed up for a trial subscription:\
**`(void)subscriptionPaymentWithTransaction:(SKPaymentTransaction *  _Nonnull)transaction product:(SKProduct *  _Nonnull)product;`**

For example:

```objectivec
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{
    for(SKPaymentTransaction *transaction in transactions) {
        switch(transaction.transactionState){
            case SKPaymentTransactionStatePurchasing: {
                // Your code ...
                break;
            }

            case SKPaymentTransactionStatePurchased: {
                // Your code ...
                SKProduct *product = [_products objectForKey:transaction.payment.productIdentifier];
                if (product != nil) {
                    [DTDAnalytics subscriptionPaymentWithTransaction:transaction product:product];
                }
                break;
            }

            case SKPaymentTransactionStateRestored: {
                // Your code ...
                break;
            }

            case SKPaymentTransactionStateFailed: {
                // Your code ...
                break;
            }
                
            case SKPaymentTransactionStateDeferred: {
                // Your code ...
                break;
            }
        }
    }
}
```

Further user actions - renewal, unsubscription, etc. are tracked by using the data received from AppStore in the server-server format. You will need the corresponding setting for it.

Also, if you want to track changes in the status of the subscriptions purchased before devtodev SDK 2.0 integration, you need to transfer your history of previously purchased subscriptions to devtodev.

The SDK monitors the need for historical data to avoid sending out excessive queries to App Store. Use the **`(void)isRestoreTransactionHistoryRequiredWithCompletionHandler:( void (^ _Nonnull)(BOOL))completionHandler;`** method to check whether or not there is a need in sending out the information about the previously purchased subscriptions to devtodev. The method returns BOOL value.

An example of a purchase history query with verification of the need for it:

```objectivec
[DTDAnalytics isRestoreTransactionHistoryRequiredWithCompletionHandler:^(BOOL isNeedRestore){
  if (isNeedRestore == true) {
    dispatch_async(dispatch_get_main_queue(), ^{
      [SKPaymentQueue.defaultQueue restoreCompletedTransactions];
    });
  }
}];
```

Use the **`(void)subscriptionHistoryWithTransactions:(NSArray<SKPaymentTransaction *> * _Nonnull)transactions;`** method to transfer the list of previously purchased subscriptions received from App Store.&#x20;

{% hint style="warning" %}
If your project accounts users by user ID (not by device ID) and the device is used by more than one user, you need to filter the transaction history so that it will contain only those transactions that belong to the active user. Otherwise, subscriptions of all device users will be attributed to the user who was the first to launch the app after the integration of subscription tracking.
{% endhint %}

```objectivec
-(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue {
  NSMutableArray *restoredTransactions = [NSMutableArray new];
    for (SKPaymentTransaction *transaction in queue.transactions) {
      if (transaction.transactionState == SKPaymentTransactionStateRestored) {
        [restoredTransactions addObject:transaction];
      }
    }
  [DTDAnalytics subscriptionHistoryWithTransactions:restoredTransactions];
}
```

{% hint style="info" %}
To recover the purchase history, the user should be logged in with his Apple ID. Be mindful of this before starting the recovering process.
{% endhint %}
{% endtab %}

{% tab title="Google Play (Kotlin)" %}
{% hint style="info" %}
Please note that in order to track subscriptions, you need to do the following:<br>

1. Call the **`subscriptionPayment`** method (described below)
2. [Configure data transfer from the Google Cloud Platform](/3rd-party-sources/app-marketplace-data/google-play-subscriptions#settings-in-google-cloud-platform-console)
3. [Configure integration in the devtodev application settings](https://docs.devtodev.com/integration/integration-of-sdk-v2/setting-up-events/pages/-MeASFB85TrtEFbUq3Pm#step-3.-settings-on-devtodev-side)
   {% endhint %}

Before sending a request for subscription from your app, during the creation of **`BillingFlowParams`**, to the **`setObfuscatedAccountId`** function of the **`BillingFlowParams.newBuilder()`** object insert **`obfuscatedAccountId`** obtained from **`DTDAnalytics.getObfuscatedAccountId`**.

{% hint style="warning" %}
Attention! The obfuscated identifier is returned asynchronously, outside of the calling thread!&#x20;
{% endhint %}

Example:

```kotlin
DTDAnalytics.getObfuscatedAccountId { obfuscatedAccountId ->
        val flowParams: BillingFlowParams = BillingFlowParams.newBuilder()
                .setObfuscatedAccountId(obfuscatedAccountId)
                .build()
            
        val result = billingClient.launchBillingFlow(activity, flowParams)
}
```

To track your subscriptions, add this event immediately after the platform confirms that the subscription was approved by the user.

```kotlin
DTDAnalytics.subscriptionPayment(orderId: String,
                                   price: Double,
                               productId: String,
                            currencyCode: String);
```

| **Parameter**      | **Type** | **Restrictions**              | **Description**                                                                                             |
| ------------------ | -------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------- |
| **`orderId`**      | string   | from 1 to 65 symbols          | A unique transaction identifier.                                                                            |
| **`currencyCode`** | string   | precisely 3 symbols           | Transaction currency ([ISO 4217 standard](https://en.wikipedia.org/wiki/ISO_4217)) e.g. ***USD, EUR*** etc. |
| **`price`**        | double   | from Double.min to Double.max | The item price in the transaction currency.                                                                 |
| **`productId`**    | string   | from 1 to 255 symbols         | Item name. We recommend using a bundle or names in the same language.                                       |

{% hint style="info" %}
How to find the transaction ID in GooglePlay transaction?

Find the INAPP\_PURCHASE\_DATA object In the JSON fields that are returned in the response data for a purchase order. A unique transaction identifier is the value of orderId property in INAPP\_PURCHASE\_DATA object. If the order is a test purchase made via the In-app Billing Sandbox, orderId property will be empty.
{% endhint %}

Further user actions - renewal, unsubscription, etc. are tracked by using the data received from Google Play in the server-server format. You will need the corresponding setting for it.

The **`subscriptionHistory`** method is used for matching users with subscribers who purchased their subscriptions before the SDK 2.0 integration. Otherwise, it will be impossible to establish the affiliation when it gets renewed or cancelled.

To get a list of active subscriptions call **`billingClient.queryPurchasesAsync`**. After successfully receiving a response from Google Play Services, pass it to **`DTDAnalytics.subscriptionHistory(purchaseList: List<String>).`**

**`purchaseList: List<String> purchaseList`** - a string containing list of json objects is passed to the **`DTDAnalytics.subscriptionHistory`** method. For the event to run, the json object must contain the following keys:

* **`orderID`** - a unique transaction identifier
* **`productID`** - a unique product identifier

The SDK monitors the need for historical data to avoid sending out excessive queries. Use the **`DTDAnalytics.isRestoreTransactionHistoryRequired`** method to check whether or not there is a need in sending out the information about the previously purchased subscriptions to devtodev. The method returns a Boolean value.

{% hint style="warning" %}
Attention! **`DTDAnalytics.isRestoreTransactionHistoryRequired`** is returned asynchronously, outside of the calling thread!
{% endhint %}

Example:

```kotlin
DTDAnalytics.isRestoreTransactionHistoryRequired { isNeedRestore ->
  if(isNeedRestore) {
    billingClient.queryPurchasesAsync(BillingClient.SkuType.SUBS) { billingResult, purchaseList ->
      if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
        val purchases = mutableListOf<String>()
        purchaseList.forEach { purchase -> purchases.add(purchase.originalJson) }
        DTDAnalytics.subscriptionHistory(purchases)
      }
    }
  }
}
```

{% hint style="warning" %}
If your project accounts users by user ID (not by device ID) and the device is used by more than one user, you need to filter the transaction history so that it will contain only those transactions that belong to the active user. Otherwise, subscriptions of all device users will be attributed to the user who was the first to launch the app after the integration of subscription tracking.
{% endhint %}
{% endtab %}

{% tab title="Google Play (Java)" %}
{% hint style="info" %}
Please note that in order to track subscriptions, you need to do the following:<br>

1. Call the **`subscriptionPayment`** method (described below)
2. [Configure data transfer from the Google Cloud Platform](/3rd-party-sources/app-marketplace-data/google-play-subscriptions#settings-in-google-cloud-platform-console)
3. [Configure integration in the devtodev application settings](https://docs.devtodev.com/integration/integration-of-sdk-v2/setting-up-events/pages/-MeASFB85TrtEFbUq3Pm#step-3.-settings-on-devtodev-side)
   {% endhint %}

Before sending a request for subscription from your app, during the creation of **`BillingFlowParams`**, to the **`setObfuscatedAccountId`** function of the **`BillingFlowParams.newBuilder()`** object insert **`obfuscatedAccountId`** obtained from **DTDAnalytics.INSTANCE.getObfuscatedAccountId**.

{% hint style="warning" %}
Attention! The obfuscated identifier is returned asynchronously, outside of the calling thread!&#x20;
{% endhint %}

Example:

```kotlin
DTDAnalytics.INSTANCE.getObfuscatedAccountId(obfuscatedAccountId -> {
              BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                      .setObfuscatedAccountId(obfuscatedAccountId)
                      .build();
                    
              billingClient.launchBillingFlow(activity, flowParams);
              return null;
      }
);
```

To track your subscriptions, add this event immediately after the platform confirms that the subscription was approved by the user.

```kotlin
DTDAnalytics.INSTANCE.subscriptionPayment(orderId: String, price: Double, productId: String, currencyCode: String);
```

| **Parameter**      | **Type** | **Restrictions**              | **Description**                                                                                             |
| ------------------ | -------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------- |
| **`orderId`**      | string   | from 1 to 65 symbols          | A unique transaction identifier.                                                                            |
| **`currencyCode`** | string   | precisely 3 symbols           | Transaction currency ([ISO 4217 standard](https://en.wikipedia.org/wiki/ISO_4217)) e.g. ***USD, EUR*** etc. |
| **`price`**        | double   | from Double.min to Double.max | The item price in the transaction currency.                                                                 |
| **`productId`**    | string   | from 1 to 255 symbols         | Item name. We recommend using a bundle or names in the same language.                                       |

{% hint style="info" %}
How to find the transaction ID in GooglePlay transaction?

Find the INAPP\_PURCHASE\_DATA object In the JSON fields that are returned in the response data for a purchase order. A unique transaction identifier is the value of orderId property in INAPP\_PURCHASE\_DATA object. If the order is a test purchase made via the In-app Billing Sandbox, orderId property will be empty.
{% endhint %}

Further user actions - renewal, unsubscription, etc. are tracked by using the data received from Google Play in the server-server format. You will need the corresponding setting for it.

The **`subscriptionHistory`** method is used for matching users with subscribers who purchased their subscriptions before the SDK 2.0 integration. Otherwise, it will be impossible to establish the affiliation when it gets renewed or cancelled.

To get a list of active subscriptions call **`billingClient.queryPurchasesAsync`**. After successfully receiving a response from Google Play Services, pass it to **`DTDAnalytics.subscriptionHistory(purchaseList: List<String>).`**

**`purchaseList: List<String> purchaseList`** - a string containing list of json objects is passed to the **`DTDAnalytics.INSTANCE.subscriptionHistory`** method. For the event to run, the json object must contain the following keys:

* **`orderID`** - a unique transaction identifier
* **`productID`** - a unique product identifier

The SDK monitors the need for historical data to avoid sending out excessive queries. Use the **`DTDAnalytics.INSTANCE.isRestoreTransactionHistoryRequired`** method to check whether or not there is a need in sending out the information about the previously purchased subscriptions to devtodev. The method returns a Boolean value.

{% hint style="warning" %}
Attention! **`DTDAnalytics.INSTANCE.isRestoreTransactionHistoryRequired`** is returned asynchronously, outside of the calling thread!
{% endhint %}

Example:

```kotlin
DTDAnalytics.INSTANCE.isRestoreTransactionHistoryRequired(isNeedRestore -> {
    if (isNeedRestore) {
        billingClient.queryPurchasesAsync(BillingClient.ProductType.SUBS, (billingResult, purchaseList) -> {
            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                ArrayList<String> purchases = new ArrayList<>();
                purchaseList.forEach(purchase ->
                        purchases.add(purchase.getOriginalJson())
                );
                DTDAnalytics.INSTANCE.subscriptionHistory(purchases);
            }
        });
    }
    return null;
});
```

{% hint style="warning" %}
If your project accounts users by user ID (not by device ID) and the device is used by more than one user, you need to filter the transaction history so that it will contain only those transactions that belong to the active user. Otherwise, subscriptions of all device users will be attributed to the user who was the first to launch the app after the integration of subscription tracking.
{% endhint %}
{% endtab %}

{% tab title="App Store+GP (Unity IAPv5)" %}
Integrate the **Subscriptions** module into your project. You can do this by manually importing the *unitypackage*.

## **Integration by importing the unitypackage**

1. Download the latest version of devtodev package from the repository: <https://github.com/devtodev-analytics/Unity-sdk-3.0/releases/latest>
2. Import DTDAnalytics.unitypackage to your project
3. Make sure **Unity IAP v5** is installed via **Package Manager.**&#x20;
4. Import DTDSubscriptions(IAPv5).unitypackage to your project. &#x20;

For the **`DTDSubscriptions`** module to function, you need the **`DTDAnalytics`** and **`Unity IAP`** modules.&#x20;

## Initialization Order&#x20;

Make sure to follow the correct initialization order (see [example](#example) below):&#x20;

1. Initialize Unity Services
2. Initialize DTDAnalytics
3. Create Unity IAP StoreController
4. Subscribe to IAP events
5. Initialize DTDSubscriptions
6. Connect to the store
7. Fetch products
8. Restore history (if required)
9. Fetch purchases

## Initialization

1. Initialize the **`DTDAnalytics`** module (see our [instruction manual](/integration/integration-of-sdk-v2/sdk-integration/unity)).&#x20;
2. Create Unity IAP StoreController, subscribe to IAP events and initialize **`DTDSubscriptions`**:&#x20;

```csharp
private async Task InitializePurchasing()
    {
        storeController = UnityIAPServices.StoreController();

        storeController.OnPurchasePending += OnPurchasePending;
        storeController.OnPurchaseFailed += OnPurchaseFailed;
        storeController.OnProductsFetched += OnProductsFetched;
        storeController.OnProductsFetchFailed += OnProductsFetchFailed;
        storeController.OnPurchasesFetched += OnPurchasesFetched;
        storeController.OnStoreDisconnected += OnStoreDisconnected;

        DTDSubscriptions.Initialize(storeController);

        await storeController.Connect();

        storeController.FetchProducts(GetInitialProducts());
    }
```

## Restore purchase history&#x20;

Restore the purchase history to ensure the module functions correctly.&#x20;

Call the **`DTDSubscriptions.History()`** method after successful IAP initialization.&#x20;

{% hint style="info" %}
To avoid excessive restoring of subscription history, use the **`DTDSubscriptions.IsRestoreTransactionHistoryRequired(Action<bool> resultCallback)`** method. If the resultCallback returns true, call the **`DTDSubscriptions.History()`** method.
{% endhint %}

Example:&#x20;

```csharp
private void OnProductsFetched(List<Product> products)
{
    DTDSubscriptions.IsRestoreTransactionHistoryRequired(restoreRequired =>
    {
        if (restoreRequired)
        {
            DTDSubscriptions.History();
        }
    });

    storeController.FetchPurchases();
}
```

## Purchase processing&#x20;

Add a **`DTDSubscriptions.Payment(Product product)`** call to the **`OnPurchasePending`** method.

```csharp
private void OnPurchasePending(PendingOrder pendingOrder)
{
    if (pendingOrder.Info.PurchasedProductInfo
        .FirstOrDefault()?.subscriptionInfo != null)
    {
        DTDSubscriptions.Payment(pendingOrder);
    }

    storeController.ConfirmPurchase(pendingOrder);
}
```

## Example

Full Initialization Example:

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Purchasing;
using Unity.Services.Core;
using Unity.Services.Core.Environments;
using DevToDev.Analytics;
using DevToDev.Subscriptions;

public class IAPManager : MonoBehaviour
{
    private static StoreController storeController;

    private async void Start()
    {
        await InitializeUnityServices();
        InitializeAnalytics();

        if (!IsIAPInitialized())
        {
            await InitializePurchasing();
        }
    }

    private async Task InitializeUnityServices()
    {
        try
        {
            var options = new InitializationOptions()
                .SetEnvironmentName("production");

            await UnityServices.InitializeAsync(options);
        }
        catch (Exception exception)
        {
            Debug.LogError($"Unity Services initialization failed: {exception}");
        }
    }

    private void InitializeAnalytics()
    {
        DTDAnalytics.SetLogLevel(DTDLogLevel.Debug);
        DTDAnalytics.Initialize(string.Empty);
    }

    private bool IsIAPInitialized()
    {
        return storeController != null;
    }

    private async Task InitializePurchasing()
    {
        storeController = UnityIAPServices.StoreController();

        storeController.OnPurchasePending += OnPurchasePending;
        storeController.OnPurchaseFailed += OnPurchaseFailed;
        storeController.OnProductsFetched += OnProductsFetched;
        storeController.OnProductsFetchFailed += OnProductsFetchFailed;
        storeController.OnPurchasesFetched += OnPurchasesFetched;
        storeController.OnStoreDisconnected += OnStoreDisconnected;

        DTDSubscriptions.Initialize(storeController);

        await storeController.Connect();

        storeController.FetchProducts(GetInitialProducts());
    }

    private List<ProductDefinition> GetInitialProducts()
    {
        return new List<ProductDefinition>
        {
            new("fake_consumable_small", ProductType.Consumable),
            new("fake_consumable_large", ProductType.Consumable),
            new("fake_subscription_monthly", ProductType.Subscription),
            new("fake_subscription_yearly", ProductType.Subscription)
        };
    }

    private void OnPurchasePending(PendingOrder pendingOrder)
    {
        if (pendingOrder.Info.PurchasedProductInfo
            .FirstOrDefault()?.subscriptionInfo != null)
        {
            DTDSubscriptions.Payment(pendingOrder);
        }

        storeController.ConfirmPurchase(pendingOrder);
    }

    private void OnPurchaseFailed(FailedOrder failedOrder)
    {
        // Process failed order
    }

    private void OnProductsFetched(List<Product> products)
    {
        DTDSubscriptions.IsRestoreTransactionHistoryRequired(restoreRequired =>
        {
            if (restoreRequired)
            {
                DTDSubscriptions.History();
            }
        });

        storeController.FetchPurchases();
    }

    private void OnProductsFetchFailed(ProductFetchFailed fetchFailed)
    {
        // Process product fetch failure
    }

    private void OnPurchasesFetched(Orders orders)
    {
        // Process confirmed orders
        // Process pending orders
        // Process deferred orders
    }

    private void OnStoreDisconnected(StoreConnectionFailureDescription failureDescription)
    {
        // Process store disconnection
    }

    private void OnDestroy()
    {
        if (storeController == null)
            return;

        storeController.OnPurchasePending -= OnPurchasePending;
        storeController.OnPurchaseFailed -= OnPurchaseFailed;
        storeController.OnProductsFetched -= OnProductsFetched;
        storeController.OnProductsFetchFailed -= OnProductsFetchFailed;
        storeController.OnPurchasesFetched -= OnPurchasesFetched;
        storeController.OnStoreDisconnected -= OnStoreDisconnected;
    }
}
```

{% endtab %}

{% tab title="App Store+GP (Unity IAPv4)" %}
Integrate the **Subscriptions** module into your project. You can do this by manually importing the *unitypackage*.

## **Integration by importing the unitypackage**

1. Download the latest version of devtodev package from the repository: <https://github.com/devtodev-analytics/Unity-sdk-3.0/releases/latest>
2. Import DTDAnalytics.unitypackage to your project
3. Import DTDSubscriptions(IAPv4).unitypackage to your project.

For the **`DTDSubscriptions`** module to function, you need the **`DTDAnalytics`** and Unity IAP modules.&#x20;

You also need to create an ***`AppleTangle`*** file (only for iOS). Open the Unity editor menu and choose ***Window → Unity IAP → Receipt Validation Obfuscator*** (pic. 1).

In case you don’t use the IAP receipt validation, clear the input field under “***2. Paste the key here:***” and click ***Obfuscate Google Play Licence Key*** (pic. 2).

![Pic. 1](/files/TnGpUyeKpQGIP46qAkjQ)

![Pic. 2](/files/annzufm8XHnwdVzvPA1Q)

## Initialization

Initialize the **`DTDAnalytics`** module (see our [instruction manual](/integration/integration-of-sdk-v2/sdk-integration/unity)).

Add **`DTDSubscriptions.Initialize(IStoreController controller)`** to the [OnInitialized ](https://docs.unity3d.com/Manual/UnityIAPInitialization.html)method.

```csharp
/// <summary>
/// Your IStoreListener implementation of OnInitialized.
/// </summary>
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
   DTDSubscriptions.Initialize(controller);
}
```

## Restore purchase history

Restore purchase history in order for the module to function correctly.

### Google Play

If you use **Google Play**, after successful IAP initialization call the **`DTDSubscriptions.History()`** method.&#x20;

{% hint style="info" %}
To avoid excessive restoring of subscription history, use the **`DTDSubscriptions.IsRestoreTransactionHistoryRequired(Action<bool> resultCallback)`** method. If the resultCallback returns true, call the **`DTDSubscriptions.History()`** method.
{% endhint %}

Example:

```csharp
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
#if UNITY_ANDROID     
     DTDSubscriptions.Initialize(controller);
     DTDSubscriptions.IsRestoreTransactionHistoryRequired((b) =>
     {
        if(b) DTDSubscriptions.History();
     });
#endif
}
```

### App Store

If you use **Apple** **App Store**, first set the **`DTDSubscriptions.IsRestoring`** property to ***true*** (this will filter out unwanted transactions). After [restoring IAP transactions](https://docs.unity3d.com/Manual/UnityIAPRestoringTransactions.html), call the **`DTDSubscriptions.History()`** method. After that, set the **`DTDSubscriptions.IsRestoring propert`**&#x79; to ***false***.

Example:

```csharp
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
    DTDSubscriptions.Initialize(controller);
if UNITY_STANDALONE_OSX || UNITY_IOS
    DTDSubscriptions.IsRestoring = true;
    extensions.GetExtension<IAppleExtensions>().RestoreTransactions(result =>
    {
        if (result)
        {
            DTDSubscriptions.IsRestoreTransactionHistoryRequired((b) =>
            {
                if (b) DTDSubscriptions.History();
            });
        }
        DTDSubscriptions.IsRestoring = false;
    });
#endif
}
```

## Purchase processing

Add a **`DTDSubscriptions.Payment(Product product)`** call to the **`ProcessPurchase`** method.

```csharp
public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs e)
{
   var product = e.purchasedProduct;
   DTDSubscriptions.Payment(product);
   return PurchaseProcessingResult.Complete;
}
```

## Example

Below you can see an example of the entire script:

```csharp
using DevToDev.Subscriptions;
using UnityEngine;
using UnityEngine.Purchasing;

public class MyIAPManager : IStoreListener
{
    public IStoreController StoreController { get; private set; }

    public void InitializeIAPManager()
    {
        var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
        builder.AddProduct("example", ProductType.Subscription);
        UnityPurchasing.Initialize(this, builder);
    }

    /// <summary>
    /// Called when Unity IAP is ready to make purchases.
    /// </summary>
    public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    {
        StoreController = controller;
        DTDSubscriptions.Initialize(controller);
#if UNITY_ANDROID
        DTDSubscriptions.IsRestoreTransactionHistoryRequired((b) =>
        {
            if (b) DTDSubscriptions.History();
        });
#elif UNITY_STANDALONE_OSX || UNITY_IOS
        DTDSubscriptions.IsRestoring = true;
        extensions.GetExtension<IAppleExtensions>().RestoreTransactions(result =>
        {
            if (result)
            {
                DTDSubscriptions.IsRestoreTransactionHistoryRequired((b) =>
                {
                    if (b) DTDSubscriptions.History();
                });
            }
            DTDSubscriptions.IsRestoring = false;
        });
#endif
    }

    /// <summary>
    /// Called when Unity IAP encounters an unrecoverable initialization error.
    ///
    /// Note that this will not be called if Internet is unavailable; Unity IAP
    /// will attempt initialization until it becomes available.
    /// </summary>
    public void OnInitializeFailed(InitializationFailureReason error)
    {
        Debug.Log($"IAP initialization error {error.ToString()}");
    }

    /// <summary>
    /// Called when a purchase completes.
    ///
    /// May be called at any time after OnInitialized().
    /// </summary>
    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
    {
        var product = e.purchasedProduct;
        DTDSubscriptions.Payment(product);
        return PurchaseProcessingResult.Complete;
    }

    /// <summary>
    /// Called when a purchase fails.
    /// </summary>
    public void OnPurchaseFailed(Product i, PurchaseFailureReason p)
    {
        Debug.Log(p.ToString());
    }
}


```

{% endtab %}
{% endtabs %}

## **Onboarding (tutorial)**

The event allows you to track tutorial completion and identify the stages where you lose new users.

We recommend tracking the starting point (value  ***-1***) before beginning the first tutorial stage, then passing the counting number of every completed stage after its completion (integers larger than 0), and at the end, marking the moment of the last tutorial stage completion (value  ***-2***).

If your app has an option of skipping the tutorial and the user has used it, then it’s necessary to send a refusal value (value ***0***) only.

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
DTDAnalytics.tutorial(step: 1)
```

The method takes on the step value with an integer type.

| Value          | Meaning                                          |
| -------------- | ------------------------------------------------ |
| ***`0`***      | The user skipped the tutorial                    |
| ***`-1`***     | The value defines the beginning of the tutorial  |
| ***`1..int`*** | Counting number of completed tutorial stage      |
| ***`-2`***     | The value defines the completion of the tutorial |
| {% endtab %}   |                                                  |

{% tab title="iOS+macOS (Objective-C)" %}

```objectivec
[DTDAnalytics tutorialStep:1];
```

The method takes on the step value with an integer type.

| Value          | Meaning                                          |
| -------------- | ------------------------------------------------ |
| ***`0`***      | The user skipped the tutorial                    |
| ***`-1`***     | The value defines the beginning of the tutorial  |
| ***`1..int`*** | Counting number of completed tutorial stage      |
| ***`-2`***     | The value defines the completion of the tutorial |
| {% endtab %}   |                                                  |

{% tab title="Android (Kotlin)" %}

```kotlin
DTDAnalytics.tutorial(step = 1)
```

The method takes on the step value with an integer type.

<table><thead><tr><th width="419.04224441097614">Value</th><th>Meaning</th></tr></thead><tbody><tr><td><em><strong><code>0</code></strong></em></td><td>The user skipped the tutorial</td></tr><tr><td><em><strong><code>-1</code></strong></em></td><td>The value defines the beginning of the tutorial</td></tr><tr><td><em><strong><code>1..int</code></strong></em></td><td>Counting number of completed tutorial stage</td></tr><tr><td><em><strong><code>-2</code></strong></em></td><td>The value defines the completion of the tutorial</td></tr></tbody></table>
{% endtab %}

{% tab title="Android (Java)" %}

```java
DTDAnalytics.INSTANCE.tutorial(1);
```

The method takes on the step value with an integer type.

<table><thead><tr><th width="419.04224441097614">Value</th><th>Meaning</th></tr></thead><tbody><tr><td><em><strong><code>0</code></strong></em></td><td>The user skipped the tutorial</td></tr><tr><td><em><strong><code>-1</code></strong></em></td><td>The value defines the beginning of the tutorial</td></tr><tr><td><em><strong><code>1..int</code></strong></em></td><td>Counting number of completed tutorial stage</td></tr><tr><td><em><strong><code>-2</code></strong></em></td><td>The value defines the completion of the tutorial</td></tr></tbody></table>
{% endtab %}

{% tab title=".NET Native + UWP" %}

```csharp
DTDAnalytics.Tutorial(1);
```

The method takes on the step value with an integer type.

| Value          | Meaning                                          |
| -------------- | ------------------------------------------------ |
| ***`0`***      | The user skipped the tutorial                    |
| ***`-1`***     | The value defines the beginning of the tutorial  |
| ***`1..int`*** | Counting number of completed tutorial stage      |
| ***`-2`***     | The value defines the completion of the tutorial |
| {% endtab %}   |                                                  |

{% tab title="Unity" %}

```csharp
DTDAnalytics.Tutorial(1);
```

The method takes on the step value with an int base type.

| Value          | Meaning                                          |
| -------------- | ------------------------------------------------ |
| ***`0`***      | The user skipped the tutorial                    |
| ***`-1`***     | The value defines the beginning of the tutorial  |
| ***`1..int`*** | Counting number of completed tutorial stage      |
| ***`-2`***     | The value defines the completion of the tutorial |
| {% endtab %}   |                                                  |

{% tab title="Web" %}

```javascript
analytics.tutorial(1)
```

The method takes on the step value with an integer type.

| Value          | Meaning                                          |
| -------------- | ------------------------------------------------ |
| ***`0`***      | The user skipped the tutorial                    |
| ***`-1`***     | The value defines the beginning of the tutorial  |
| ***`1..int`*** | Counting number of completed tutorial stage      |
| ***`-2`***     | The value defines the completion of the tutorial |
| {% endtab %}   |                                                  |

{% tab title="Unreal" %}
![Blueprint](/files/ds9agzCX33QUN291UnCn)

| Parameter  | Type  | Restrictions                 | Description   |
| ---------- | ----- | ---------------------------- | ------------- |
| **`step`** | int32 | From 1 to int32.MaxValue - 1 | Tutorial step |

```cpp
UDTDAnalyticsBPLibrary::Tutorial(1);
```

The method takes on the step value with an int32 base type.

| Value            | Meaning                                          |
| ---------------- | ------------------------------------------------ |
| ***`0`***        | The user skipped the tutorial                    |
| ***`-1`***       | The value defines the beginning of the tutorial  |
| ***`1..int32`*** | Counting number of completed tutorial stage      |
| ***`-2`***       | The value defines the completion of the tutorial |
| {% endtab %}     |                                                  |

{% tab title="Godot" %}

```gdscript
DTDAnalytics.Tutorial(1)
```

The method takes on the step value with an integer type.

| Value          | Meaning                                          |
| -------------- | ------------------------------------------------ |
| ***`0`***      | The user skipped the tutorial                    |
| ***`-1`***     | The value defines the beginning of the tutorial  |
| ***`1..int`*** | Counting number of completed tutorial stage      |
| ***`-2`***     | The value defines the completion of the tutorial |
| {% endtab %}   |                                                  |
| {% endtabs %}  |                                                  |

## Level up

This event is for games only. It is worthwhile to integrate this event into a game type project, as specified in the [application settings](/getting-started/adding-an-app-to-the-space). In projects with the “app” type, game events will not be tracked and displayed in the interface, even if they are integrated. You can verify and change the project type in Settings → [General settings](/reports-and-functionality/project-related-reports-and-fuctionality/settings#general-settings).

The event allows you to analyze the distribution of players over different game levels, monitor the in-game currency balance by levels. You can find more information about the right moment to use **`LevelUp`** event [here](/integration/expert-tips/what-to-track#if-users-in-your-project-become-more-experienced-and-raise-their-level).&#x20;

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}
The event should be dispatched right after the level-up. The number of the level reached is passed to the **`level`** parameter.

```swift
DTDAnalytics.levelUp(level: 2)
```

To monitor the average account balance of in-game currency by the end of each level, dispatch in-game currencies (resources) names and their amounts to the method signature:

```swift
let balance: [String: Int] = ["Currency name 1": 100, "Currency name 2": 10]
DTDAnalytics.levelUp(level: 2, balances: balance)
```

{% hint style="warning" %}
Attention! The number of tracked in-game currencies or resources (their unique names) should not exceed 50 at all times. See [Limits](/data-management-and-limits).
{% endhint %}

| Parameter        | Type          | Restrictions                                                                 | Description                                         |
| ---------------- | ------------- | ---------------------------------------------------------------------------- | --------------------------------------------------- |
| ***`level`***    | int           | From 1 to Int32.max - 1                                                      | Level reached                                       |
| ***`balances`*** | \[String:Int] | <p>String - from 1 to 24 symbols</p><p>Int - from Int64.min to int64.max</p> | Resources’ names and number at the time of level up |
| {% endtab %}     |               |                                                                              |                                                     |

{% tab title="iOS+macOS (Objective-C)" %}
The event should be dispatched right after the level-up. The number of the level reached is passed to the **`level`** parameter.

```objectivec
[DTDAnalytics levelUp:2];
```

To monitor the average account balance of in-game currency by the end of each level, dispatch in-game currencies (resources) names and their amounts to the method signature:

```objectivec
NSDictionary *balance = @{@"Currency name 1": @100, @"Currency name 2": @10};
[DTDAnalytics levelUp:2 withBalances:balance];
```

{% hint style="warning" %}
Attention! The number of tracked in-game currencies or resources (their unique names) should not exceed 50 at all times. See [Limits](/data-management-and-limits).
{% endhint %}

| Parameter        | Type                                   | Restrictions                                                                 | Description                                         |
| ---------------- | -------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------- |
| ***`level`***    | NSInteger                              | From 1 to Int32.max - 1                                                      | Level reached                                       |
| ***`balances`*** | NSDictionary\<NSString \*,NSNumber \*> | <p>String - from 1 to 24 symbols</p><p>Int - from Int64.min to int64.max</p> | Resources’ names and number at the time of level up |
| {% endtab %}     |                                        |                                                                              |                                                     |

{% tab title="Android (Kotlin)" %}
The event should be dispatched right after the level-up. The number of the level reached is passed to the **`level`** parameter.

```kotlin
DTDAnalytics.levelUp(level = 2)
```

To monitor the average account balance of in-game currency by the end of each level, dispatch in-game currencies (resources) names and their amounts to the method signature:

```kotlin
val balances = mapOf("Currency name 1" to 100L, "Currency name 2" to 10L)
DTDAnalytics.levelUp(
    level = 2, 
    balance = balances
)
```

{% hint style="warning" %}
Attention! The number of tracked in-game currencies or resources (their unique names) should not exceed 50 at all times. See [Limits](/data-management-and-limits).
{% endhint %}

| Parameter        | Type              | Restrictions                                                                | Description                                         |
| ---------------- | ----------------- | --------------------------------------------------------------------------- | --------------------------------------------------- |
| ***`level`***    | int               | From 1 to Int.max - 1                                                       | Level reached                                       |
| ***`balances`*** | Map\<String,Long> | <p>String - from 1 to 24 symbols</p><p>Long - from Long.min to Long.max</p> | Resources’ names and number at the time of level up |
| {% endtab %}     |                   |                                                                             |                                                     |

{% tab title="Android (Java)" %}
The event should be dispatched right after the level-up. The number of the level reached is passed to the **`level`** parameter.

```java
DTDAnalytics.INSTANCE.levelUp(2);
```

To monitor the average account balance of in-game currency by the end of each level, dispatch in-game currencies (resources) names and their amounts to the method signature:&#x20;

```java
Map<String, Long> balances = new HashMap<>();
balances.put("Currency name 1", 100L);
balances.put("Currency name 2", 10L);
DTDAnalytics.INSTANCE.levelUp(2, balances);
```

{% hint style="warning" %}
Attention! The number of tracked in-game currencies or resources (their unique names) should not exceed 50 at all times. See [Limits](/data-management-and-limits).
{% endhint %}

| Parameter        | Type              | Restrictions                                                                | Description                                         |
| ---------------- | ----------------- | --------------------------------------------------------------------------- | --------------------------------------------------- |
| ***`level`***    | int               | From 1 to Int.max - 1                                                       | Level reached                                       |
| ***`balances`*** | Map\<String,Long> | <p>String - from 1 to 24 symbols</p><p>Long - from Long.min to Long.max</p> | Resources’ names and number at the time of level up |
| {% endtab %}     |                   |                                                                             |                                                     |

{% tab title=".NET Native + UWP" %}
The event should be dispatched right after the level-up. The number of the level reached is passed to the **`level`** parameter.

```csharp
DTDAnalytics.LevelUp(2);
```

To monitor the average account balance of in-game currency by the end of each level, dispatch in-game currencies (resources) names and their amounts to the method signature:&#x20;

```csharp
var balance = new Dictionary<string, long>();
balance.Add("Currency name 1", 100);
balance.Add("Currency name 2", 200);
DTDAnalytics.LevelUp(2, balance);
```

{% hint style="warning" %}
Attention! The number of tracked in-game currencies or resources (their unique names) should not exceed 50 at all times. See [Limits](/data-management-and-limits).
{% endhint %}

| Parameter        | Type            | Restrictions                                                                          | Description                                         |
| ---------------- | --------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------- |
| ***`level`***    | int             | From 1 to int.MaxValue - 1                                                            | Level reached                                       |
| ***`balances`*** | \[string: long] | <p>String - from 1 to 24 symbols</p><p>Long - from long.MinValue to long.MaxValue</p> | Resources’ names and number at the time of level up |
| {% endtab %}     |                 |                                                                                       |                                                     |

{% tab title="Unity" %}
The event should be dispatched right after the level-up. The number of the level reached is passed to the ***`level`*** parameter.

```csharp
DTDAnalytics.LevelUp(level: 2)
```

To monitor the average account balance of in-game currency by the end of each level, dispatch in-game currencies (resources) names and their amounts to the method signature:&#x20;

```csharp
var balance = new Dictionary<string, long>();
balance.Add("Currency name 1", 100);
balance.Add("Currency name 2", 200);
DTDAnalytics.LevelUp(level: 2, balances: balance);
```

{% hint style="warning" %}
Attention! The number of tracked in-game currencies or resources (their unique names) should not exceed 50 at all times. See [Limits](/data-management-and-limits).
{% endhint %}

| Parameter        | Type            | Restrictions                                                                              | Description                                         |
| ---------------- | --------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------- |
| ***`level`***    | int             | From 1 to int32.MaxValue - 1                                                              | Level reached                                       |
| ***`balances`*** | \[string: long] | <p>String - from 1 to 24 symbols</p><p>Long - from long64.MinValue to long64.MaxValue</p> | Resources’ names and number at the time of level up |
| {% endtab %}     |                 |                                                                                           |                                                     |

{% tab title="Web" %}
The event should be dispatched right after the level-up. The number of the level reached is passed to the **`level`** parameter.

```javascript
analytics.levelUp(2)
```

You can send and track the following data along with the level values: an average amount of the in-game currency by the end of the level, user spendings on the level, and amounts of purchased or earned in-game currency/resources.\
Unfortunately, Web SDK doesn’t allow to automatically calculate spending and receiving of the in-game currency/resources while users are passing the level (data accumulation on the Web might be inaccurate as users might utilize multiple browsers and devices, as well as erase local browser data).&#x20;

```javascript
const balance = {
                "Currency name 1" : 100,
                "Currency name 2" : 200
}
const spent = {
                "Currency name 2" : 1
}
const earned = {
                                
                "Currency name 1" : 5
}
const bought = {
                "Currency name 1" : 50,
                "Currency name 2" : 30
}
analytics.levelUp(2, balance, spent,  earned, bought)
```

{% hint style="warning" %}
Attention! The number of tracked in-game currencies or resources (their unique names) should not exceed 50 at all times. See [Limits](/data-management-and-limits).
{% endhint %}

| Parameter        | Type            | Restrictions                                                                          | Description                                             |
| ---------------- | --------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| ***`level`***    | int             | From 1 to int.MaxValue - 1                                                            | Level reached                                           |
| ***`balances`*** | \[string: long] | <p>String - from 1 to 24 symbols</p><p>Long - from long.MinValue to long.MaxValue</p> | Resources’ names and number at the time of level up     |
| ***`spent`***    | \[string: long] | <p>String - from 1 to 24 symbols<br>Long - from 0 to Number.MAX\_SAFE\_INTEGER</p>    | Game currency amount spent during the level. Optional.  |
| ***`earned`***   | \[string: long] | <p>String - from 1 to 24 symbols<br>Long - from 0 to Number.MAX\_SAFE\_INTEGER</p>    | Game currency earned during the level. Optional.        |
| ***`bought`***   | \[string: long] | <p>String - from 1 to 24 symbols<br>Long - from 0 to Number.MAX\_SAFE\_INTEGER</p>    | Game currency amount bought during the level. Optional. |
| {% endtab %}     |                 |                                                                                       |                                                         |

{% tab title="Unreal" %}
The event should be dispatched right after the level-up. The number of the level reached is passed to the ***`level`*** parameter.

![Blueprint](/files/16tvjRkNWAdMoiyDLzkM)

| Parameter     | Type  | Restrictions                 | Description   |
| ------------- | ----- | ---------------------------- | ------------- |
| ***`level`*** | int32 | From 1 to int32.MaxValue - 1 | Level reached |

```cpp
UDTDAnalyticsBPLibrary::LevelUp(2);
```

To monitor the average account balance of in-game currency by the end of each level, dispatch in-game currencies (resources) names and their amounts to the method signature:&#x20;

![Blueprint](/files/2bIsjythMcnKO7Jk4hVH)

{% hint style="warning" %}
Attention! The number of tracked in-game currencies or resources (their unique names) should not exceed 50 at all times. See [Limits](/data-management-and-limits).
{% endhint %}

| Parameter       | Type                  | Restrictions                                                                              | Description                                         |
| --------------- | --------------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------- |
| ***`level`***   | int32                 | From 1 to int32.MaxValue - 1                                                              | Level reached                                       |
| ***`balance`*** | TMap\<FString, int64> | <p>FString - from 1 to 24 symbols</p><p>int64 - from int64.MinValue to int64.MaxValue</p> | Resources’ names and number at the time of level up |

```cpp
TMap<FString, int64> balance;
balance.Add("CurrencyName", 123);
UDTDAnalyticsBPLibrary::LevelUpWithBalance(2, balance);
```

{% endtab %}

{% tab title="Godot" %}
The event should be dispatched right after the level-up. The number of the level reached is passed to the **`level`** parameter.

```gdscript
DTDAnalytics.LevelUp(2)
```

To monitor the average account balance of in-game currency by the end of each level, dispatch in-game currencies (resources) names and their amounts to the method signature:&#x20;

```gdscript
var balances = GDDTDInt64Resources.new()
balances.AddValue("level_resource_1", 6000)
balances.AddValue("level_resource_2", 95001000)
DTDAnalytics.LevelUpWithBalance(2, balances)
```

{% hint style="warning" %}
Attention! The number of tracked in-game currencies or resources (their unique names) should not exceed 50 at all times. See [Limits](/data-management-and-limits).
{% endhint %}

<table><thead><tr><th width="148">Parameter</th><th width="129">Type</th><th width="201">Restrictions</th><th>Description</th></tr></thead><tbody><tr><td><em><strong><code>level</code></strong></em></td><td>int</td><td>From 1 to Int32.max - 1</td><td>Level reached</td></tr><tr><td><em><strong><code>balances</code></strong></em></td><td>GDDTDInt64Resources</td><td><p>String - from 1 to 24 symbols</p><p>Int - from Int64.min to int64.max</p></td><td>Resources’ names and number at the time of level up</td></tr></tbody></table>
{% endtab %}
{% endtabs %}

## Current Balance

This event is for games only. It is worthwhile to integrate this event into a game-type project, as specified in the [application settings](/getting-started/adding-an-app-to-the-space). In projects with the “app” type, game events will not be tracked and displayed in the interface, even if they are integrated. You can verify and change the project type in Settings → [General settings](/reports-and-functionality/project-related-reports-and-fuctionality/settings#general-settings).

To track the average balance of in-game currency disregarding the level up event, pass the list of in-game currency (resource) names and their amount to the method signature:

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
let balance: [String: Int] = ["Currency name 1": 100, "Currency name 2": 10]
DTDAnalytics.currentBalance(balance: balance)
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

```objectivec
 NSDictionary *balance = @{@"Currency name 1": @100, 
                           @"Currency name 2": @10};
 [DTDAnalytics currentBalanceWithBalance:balance];
```

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
val balance = mapOf("Currency name 1" to 100L, 
                    "Currency name 2" to 10L)
DTDAnalytics.currentBalance(
    balance = balance
)
```

{% endtab %}

{% tab title="Android (Java)" %}

```java
Map<String, Long> balance = new HashMap<>();
balance.put("Currency name 1", 100L);
balance.put("Currency name 2", 10L);
DTDAnalytics.INSTANCE.currentBalance(balance);
```

{% endtab %}

{% tab title=".NET Native + UWP" %}

```csharp
var balance = new Dictionary<string, long>();
balance.Add("Currency name 1", 100);
balance.Add("Currency name 2", 200);
DTDAnalytics.CurrentBalance(balance);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
var balance = new Dictionary<string, long>();
balance.Add("Currency name 1", 100);
balance.Add("Currency name 2", 200);
DTDAnalytics.CurrentBalance(balance);
```

{% endtab %}

{% tab title="Web" %}

```csharp
const balance = {
                "Currency name 1" : 100,
                "Currency name 2" : 10
}
analytics.currentBalance(balance);
```

{% endtab %}

{% tab title="Unreal" %}
![Blueprint](/files/oQ5pvguOWXNzG9GQ2M58)

| Parameter       | Type                  | Restrictions                                                                              | Description                 |
| --------------- | --------------------- | ----------------------------------------------------------------------------------------- | --------------------------- |
| ***`balance`*** | TMap\<FString, int64> | <p>FString - from 1 to 24 symbols</p><p>int64 - from int64.MinValue to int64.MaxValue</p> | Resources’ names and number |

```cpp
TMap<FString, int64> balance;
balance.Add("CurrencyName", 123);
UDTDAnalyticsBPLibrary::CurrentBalance(balance);
```

{% endtab %}

{% tab title="Godot" %}

```gdscript
var balance = GDDTDInt64Resources.new()
balance.AddValue("current_balance_res_1", 222)
balance.AddValue("current_balance_res_2", 1500)
DTDAnalytics.CurrentBalance(balance)
```

{% endtab %}
{% endtabs %}

## Currency Accrual

This event is for games only. It is worthwhile to integrate this event into a game type project, as specified in the [application settings](/getting-started/adding-an-app-to-the-space). In projects with the “app” type, game events will not be tracked and displayed in the interface, even if they are integrated. You can verify and change the project type in Settings → [General settings](/reports-and-functionality/project-related-reports-and-fuctionality/settings#general-settings).

You need to dispatch the event after every game account balance refill if you want to track the average in-game currency amount acquired or earned by the players for a certain timeframe or during a level playthrough.

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
DTDAnalytics.currencyAccrual(currencyName: "Currency name 1", 
                             currencyAmount: 100, 
                             source: "Source name", 
                             accrualType: .earned)
```

<table data-full-width="false"><thead><tr><th>Parameter</th><th>Type</th><th>Restrictions</th><th>Description</th></tr></thead><tbody><tr><td><em><strong><code>currencyName</code></strong></em></td><td>string</td><td>from 1 to 24 symbols</td><td>In-game currency/resource name</td></tr><tr><td><em><strong><code>currencyAmount</code></strong></em></td><td>int</td><td>from 1 to Int32.max</td><td>Amount of currency in circulation</td></tr><tr><td><em><strong><code>source</code></strong></em></td><td>string</td><td>from 1 to 23 symbols</td><td>The sources of currency/resources. It can be used for breaking down income by its sources. For example, a city builder game may have some: “Rent” for the profit received from rental property, or “Bank” if the player has purchased some currency.</td></tr><tr><td><em><strong><code>accrualType</code></strong></em></td><td>DTDAccrualType (enum)</td><td></td><td>The currency/resource source type. The player can either gain resources during the game (<strong><code>earned</code></strong>) or purchase them for money (<strong><code>bought</code></strong>)</td></tr></tbody></table>

**`acrualType`** can receive one of the following values:

```swift
public enum DTDAccrualType: Int {
    case earned = 0
    case bought = 1
}
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

```objectivec
[DTDAnalytics currencyName:@"Currency name 1"
              currencyAmount:100
              source:@"Source name"
              accrualType:DTDAccrualTypeEarned];
```

| Parameter              | Type                  | Restrictions         | Description                                                                                                                                                                                                                                          |
| ---------------------- | --------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`currencyName`***   | NSString              | from 1 to 24 symbols | In-game currency/resource name                                                                                                                                                                                                                       |
| ***`currencyAmount`*** | NSInteger             | from 1 to Int32.max  | Amount of currency in circulation                                                                                                                                                                                                                    |
| ***`source`***         | NSString              | from 1 to 23 symbols | The sources of currency/resources. It can be used for breaking down income by its sources. For example, a city builder game may have some: “Rent” for the profit received from rental property, or “Bank” if the player has purchased some currency. |
| ***`accrualType`***    | DTDAccrualType (enum) |                      | The currency/resource source type. The player can either gain resources during the game (**`earned`**) or purchase them for money (**`bought`**)                                                                                                     |

**`acrualType`** can receive one of the following values:

```objectivec
typedef enum DTDAccrualType: NSUInteger {
DTDAccrualTypeEarned = 0,
DTDAccrualTypeBought = 1,
} DTDAccrualType;
```

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
DTDAnalytics.currencyAccrual(
    currencyName = "Currency name 1",
    currencyAmount = 100,
    source = "Source name",
    DTDAccrualType = DTDAccrualType.Earned
)
```

| Parameter            | Type                  | Restrictions         | Description                                                                                                                                                                                                                                          |
| -------------------- | --------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`currencyName`*** | string                | from 1 to 24 symbols | In-game currency/resource name                                                                                                                                                                                                                       |
| **`currencyAmount`** | int                   | from 1 to Int.max    | Amount of currency in circulation                                                                                                                                                                                                                    |
| ***`source`***       | string                | from 1 to 23 symbols | The sources of currency/resources. It can be used for breaking down income by its sources. For example, a city builder game may have some: “Rent” for the profit received from rental property, or “Bank” if the player has purchased some currency. |
| ***`accrualType`***  | DTDAccrualType (enum) |                      | The currency/resource source type. The player can either gain resources during the game (**`earned`**) or purchase them for money (**`bought`**)                                                                                                     |

**`accrualType`** can receive one of the following values:

```kotlin
enum class DTDAccrualType(val value: Long) {
    Earned(0L),
    Bought(1L);
}
```

{% endtab %}

{% tab title="Android (Java)" %}

```java
DTDAnalytics.INSTANCE.currencyAccrual(
        "Currency name 1",
        100, 
        "Source name",
        DTDAccrualType.Earned
);
```

| Parameter            | Type                  | Restrictions         | Description                                                                                                                                                                                                                                          |
| -------------------- | --------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`currencyName`*** | string                | from 1 to 24 symbols | In-game currency/resource name                                                                                                                                                                                                                       |
| **`currencyAmount`** | int                   | from 1 to Int.max    | Amount of currency in circulation                                                                                                                                                                                                                    |
| ***`source`***       | string                | from 1 to 23 symbols | The sources of currency/resources. It can be used for breaking down income by its sources. For example, a city builder game may have some: “Rent” for the profit received from rental property, or “Bank” if the player has purchased some currency. |
| ***`accrualType`***  | DTDAccrualType (enum) |                      | The currency/resource source type. The player can either gain resources during the game (**`earned`**) or purchase them for money (**`bought`**)                                                                                                     |

**`accrualType`** can receive one of the following values:

```java
public final enum class DTDAccrualType {
    Earned;
    Bought;
}
```

{% endtab %}

{% tab title=".NET Native + UWP" %}

```csharp
DTDAnalytics.CurrencyAccrual(
    currencyName: "Currency name 1",
    currencyAmount: 100,
    source: "Source name",
    accrualType: DTDAccrualType.Earned); 

```

| Parameter              | Type                  | Restrictions           | Description                                                                                                                                                                                                                                          |
| ---------------------- | --------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`currencyName`***   | string                | from 1 to 24 symbols   | In-game currency/resource name                                                                                                                                                                                                                       |
| ***`currencyAmount`*** | int                   | from 1 to Int.MaxValue | Amount of currency in circulation                                                                                                                                                                                                                    |
| ***`source`***         | string                | from 1 to 23 symbols   | The sources of currency/resources. It can be used for breaking down income by its sources. For example, a city builder game may have some: “Rent” for the profit received from rental property, or “Bank” if the player has purchased some currency. |
| ***`accrualType`***    | DTDAccrualType (enum) |                        | The currency/resource source type. The player can either gain resources during the game (**`earned`**) or purchase them for money (**`bought`**)                                                                                                     |

**`AccrualType`** can receive one of the following values:

```csharp
enum DTDAccrualType : long
{
    Earned = 0L,
    Bought = 1L
}
```

{% endtab %}

{% tab title="Unity" %}

```csharp
DTDAnalytics.CurrencyAccrual(currencyName: "Currency name 1", 
                             currencyAmount: 100, 
                             source: "Source name", 
                             accrualType: DTDAccrualType.Earned)

```

| Parameter              | Type                  | Restrictions             | Description                                                                                                                                                                                                                                          |
| ---------------------- | --------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`currencyName`***   | string                | from 1 to 24 symbols     | In-game currency/resource name                                                                                                                                                                                                                       |
| ***`currencyAmount`*** | int                   | from 1 to Int32.MaxValue | Amount of currency in circulation                                                                                                                                                                                                                    |
| ***`source`***         | string                | from 1 to 23 symbols     | The sources of currency/resources. It can be used for breaking down income by its sources. For example, a city builder game may have some: “Rent” for the profit received from rental property, or “Bank” if the player has purchased some currency. |
| ***`accrualType`***    | DTDAccrualType (enum) |                          | The currency/resource source type. The player can either gain resources during the game (**`earned`**) or purchase them for money (**`bought`**)                                                                                                     |

**`AccrualType`** can receive one of the following values:

```csharp
public enum DTDAccrualType
{
  Earned = 0,
  Bought = 1
}
```

{% endtab %}

{% tab title="Web" %}

```javascript
analytics.currencyAccrual(
                                currencyName,
                                currencyAmount,
                                source,
                                accrualType)
```

| Parameter              | Type   | Restrictions         | Description                                                                                                                                                                                                                                          |
| ---------------------- | ------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`currencyName`***   | string | from 1 to 24 symbols | In-game currency/resource name                                                                                                                                                                                                                       |
| ***`currencyAmount`*** | int    | from 1 to Int.max    | Amount of currency in circulation                                                                                                                                                                                                                    |
| ***`source`***         | string | from 1 to 23 symbols | The sources of currency/resources. It can be used for breaking down income by its sources. For example, a city builder game may have some: “Rent” for the profit received from rental property, or “Bank” if the player has purchased some currency. |
| ***`accrualType`***    | int    |                      | The currency/resource source type. The player can either gain resources during the game (***`0`***) or purchase them for money (***`1`***)                                                                                                           |

Example:

```javascript
analytics.currencyAccrual(
                                "Currency name 1",
                                100,
                                "Source name",
                                0)
```

{% endtab %}

{% tab title="Unreal" %}
![Blueprint](/files/eiGQPah4Ho9yV36d2QSp)

| Parameter              | Type            | Restrictions             | Description                                                                                                                                                                                                                                          |
| ---------------------- | --------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`currencyName`***   | FString         | from 1 to 24 symbols     | In-game currency/resource name                                                                                                                                                                                                                       |
| ***`currencyAmount`*** | int32           | from 1 to Int32.MaxValue | Amount of currency in circulation                                                                                                                                                                                                                    |
| ***`source`***         | FString         | from 1 to 23 symbols     | The sources of currency/resources. It can be used for breaking down income by its sources. For example, a city builder game may have some: “Rent” for the profit received from rental property, or “Bank” if the player has purchased some currency. |
| ***`accrualType`***    | EDTDAccrualType |                          | The currency/resource source type. The player can either gain resources during the game (**`earned`**) or purchase them for money (**`bought`**)                                                                                                     |

```cpp
UDTDAnalyticsBPLibrary::CurrencyAccrual("CurrencyName", 12, "Source", EDTDAccrualType::Bought);
```

{% endtab %}

{% tab title="Godot" %}

```gdscript
DTDAnalytics.CurrencyAccrual("currencyName_1", 500, "store", GDDTDAccrualType.Bought)
DTDAnalytics.CurrencyAccrual("currencyName_2", 10, "market", GDDTDAccrualType.Earned)
```

| Parameter              | Type                    | Restrictions         | Description                                                                                                                                                                                                                                          |
| ---------------------- | ----------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`currencyName`***   | String                  | from 1 to 24 symbols | In-game currency/resource name                                                                                                                                                                                                                       |
| ***`currencyAmount`*** | int                     | from 1 to Int32.max  | Amount of currency in circulation                                                                                                                                                                                                                    |
| ***`source`***         | String                  | from 1 to 23 symbols | The sources of currency/resources. It can be used for breaking down income by its sources. For example, a city builder game may have some: “Rent” for the profit received from rental property, or “Bank” if the player has purchased some currency. |
| ***`accrualType`***    | GDDTDAccrualType (enum) |                      | The currency/resource source type. The player can either gain resources during the game (**`earned`**) or purchase them for money (**`bought`**)                                                                                                     |

**`acrualType`** can receive one of the following values:

```swift
enum  AccrualType:
    Earned = 0
    Bought = 1
```

{% endtab %}
{% endtabs %}

## Virtual Currency Payment

This event is for games only. It is worthwhile to integrate this event into a game type project, as specified in the [application settings](/getting-started/adding-an-app-to-the-space). In projects with the “app” type, game events will not be tracked and displayed in the interface, even if they are integrated. You can verify and change the project type in Settings → [General settings](/reports-and-functionality/project-related-reports-and-fuctionality/settings#general-settings).

Pass this event after every purchase if you want to track in-game currency spends and items’ popularity. You can apply this event to both games and any apps with virtual currency.

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```kotlin
DTDAnalytics.virtualCurrencyPayment(purchaseId: "Purchase ID",
                                    purchaseType: "Purchase type",
                                    purchaseAmount: 100,
                                    purchasePrice: 10,
                                    purchaseCurrency: "Purchase currency")
```

In case the item is sold for more than one currency/resource, you need to build a dictionary with all the names and amounts of the currencies/resources.

```kotlin
let resources: [String: Int] = ["Purchase currency name 1": 100, 
                                "Purchase currency name 2": 10]
DTDAnalytics.virtualCurrencyPayment(purchaseId: "Purchase ID",
                                    purchaseType: "Purchase Type",
                                    purchaseAmount: 100,
                                    resources: resources)
```

| Parameter                | Type   | Restrictions         | Description                                                                                                                              |
| ------------------------ | ------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| ***`purchaseId`***       | string | from 1 to 32 symbols | A unique purchase name or ID. Make sure that the names are always in the same language otherwise they will be listed as different items. |
| ***`purchaseType`***     | string | from 1 to 96 symbols | The name of a resource group. For example, for “Wood” it can be “Construction materials”.                                                |
| ***`purchaseAmount`***   | int    | from 1 to Int32.max  | The number of units of goods purchased.                                                                                                  |
| ***`purchaseCurrency`*** | string | from 1 to 24 symbols | The name of a currency used for the purchase.                                                                                            |
| ***`purchasePrice`***    | int    | from 1 to Int32.max  | The price of the purchased item in the specified in-game currency.                                                                       |
| {% endtab %}             |        |                      |                                                                                                                                          |

{% tab title="iOS+macOS (Objective-C)" %}

```objectivec
[DTDAnalytics virtualCurrencyPaymentWithPurchaseId:@"Purchase ID"
                                      purchaseType:@"Purchase type"
                                    purchaseAmount:100
                                     purchasePrice:10
                                  purchaseCurrency:@"Purchase currency"];
```

In case the item is sold for more than one currency/resource, you need to build a dictionary with all the names and amounts of the currencies/resources.

```objectivec
NSDictionary *resources = @{@"Purchase currency name 1": @100,
                            @"Purchase currency name 2": @10};
[DTDAnalytics virtualCurrencyPaymentWithPurchaseId:@"Purchase ID"
                                      purchaseType:@"Purchase Type"
                                    purchaseAmount:100
                                         resources:resources];
```

| Parameter                | Type      | Restrictions         | Description                                                                                                                              |
| ------------------------ | --------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| ***`purchaseId`***       | NSString  | from 1 to 32 symbols | A unique purchase name or ID. Make sure that the names are always in the same language otherwise they will be listed as different items. |
| ***`purchaseType`***     | NSString  | from 1 to 96 symbols | The name of a resource group. For example, for “Wood” it can be “Construction materials”.                                                |
| ***`purchaseAmount`***   | NSInteger | from 1 to Int32.max  | The number of units of goods purchased.                                                                                                  |
| ***`purchaseCurrency`*** | NSString  | from 1 to 24 symbols | The name of a currency used for the purchase.                                                                                            |
| ***`purchasePrice`***    | NSInteger | from 1 to Int32.max  | The price of the purchased item in the specified in-game currency.                                                                       |
| {% endtab %}             |           |                      |                                                                                                                                          |

{% tab title="Android (Kotlin)" %}

```kotlin
DTDAnalytics.virtualCurrencyPayment(
    purchaseId = "Purchase ID",
    purchaseType = "Purchase type",
    purchaseAmount = 100,
    purchasePrice = 10,
    purchaseCurrency = "Purchase currency"
)
```

In case the item is sold for more than one currency/resource, you need to build a dictionary with all the names and amounts of the currencies/resources.

```kotlin
val resources = mapOf(
    "Purchase currency name 1" to 100,
    "purchase Currency name 2" to 10
)
ot
DTDAnalytics.virtualCurrencyPayment(
    purchaseId = "Purchase ID",
    purchaseType = "Purchase Type",
    purchaseAmount = 100,
    map = resources
)
```

| Parameter                | Type   | Restrictions         | Description                                                                                                                              |
| ------------------------ | ------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| ***`purchaseId`***       | string | from 1 to 32 symbols | A unique purchase name or ID. Make sure that the names are always in the same language otherwise they will be listed as different items. |
| ***`purchaseType`***     | string | from 1 to 96 symbols | The name of a resource group. For example, for “Wood” it can be “Construction materials”.                                                |
| ***`purchaseAmount`***   | int    | from 1 to Int.max    | The number of units of goods purchased.                                                                                                  |
| ***`purchaseCurrency`*** | string | from 1 to 24 symbols | The name of a currency used for the purchase.                                                                                            |
| **`purchasePrice`**      | int    | from 1 to Int.max    | The price of the purchased item in the specified in-game currency.                                                                       |
| {% endtab %}             |        |                      |                                                                                                                                          |

{% tab title="Android (Java)" %}

```java
DTDAnalytics.INSTANCE.virtualCurrencyPayment(
        "Purchase ID",
        "Purchase type",
        100,
        10,
        "Purchase currency" 
);
```

In case the item is sold for more than one currency/resource, you need to build a dictionary with all the names and amounts of the currencies/resources.

```java
Map<String, Integer> resources = new HashMap<>();
resources.put("Purchase currency name 1", 100);
resources.put("Purchase currency name 2", 10);
DTDAnalytics.INSTANCE.virtualCurrencyPayment(
        "Purchase ID",
        "Purchase Type",
        100,
        resources
);
```

| Parameter                | Type   | Restrictions         | Description                                                                                                                              |
| ------------------------ | ------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| ***`purchaseId`***       | string | from 1 to 32 symbols | A unique purchase name or ID. Make sure that the names are always in the same language otherwise they will be listed as different items. |
| ***`purchaseType`***     | string | from 1 to 96 symbols | The name of a resource group. For example, for “Wood” it can be “Construction materials”.                                                |
| ***`purchaseAmount`***   | int    | from 1 to Int.max    | The number of units of goods purchased.                                                                                                  |
| ***`purchaseCurrency`*** | string | from 1 to 24 symbols | The name of a currency used for the purchase.                                                                                            |
| **`purchasePrice`**      | int    | from 1 to Int.max    | The price of the purchased item in the specified in-game currency.                                                                       |
| {% endtab %}             |        |                      |                                                                                                                                          |

{% tab title=".NET Native + UWP" %}

```csharp
DTDAnalytics.VirtualCurrencyPayment(
    purchaseId: "Purchase ID",
    purchaseType: "Purchase type",
    purchaseAmount: 100,
    purchasePrice: 10,
    purchaseCurrency: "Purchase currency");
```

In case the item is sold for more than one currency/resource, you need to build a dictionary with all the names and amounts of the currencies/resources.

```csharp
var resources = new Dictionary<string, int>
{
    ["Purchase currency name 1"] = 100,
    ["purchase Currency name 2"] = 10
};
DTDAnalytics.VirtualCurrencyPayment(
    purchaseId: "Purchase ID",
    purchaseType: "Purchase Type",
    purchaseAmount: 100,
    resources: resources);
```

| Parameter                | Type   | Restrictions           | Description                                                                                                                              |
| ------------------------ | ------ | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| ***`purchaseId`***       | string | from 1 to 32 symbols   | A unique purchase name or ID. Make sure that the names are always in the same language otherwise they will be listed as different items. |
| ***`purchaseType`***     | string | from 1 to 96 symbols   | The name of a resource group. For example, for “Wood” it can be “Construction materials”.                                                |
| ***`purchaseAmount`***   | int    | from 1 to int.MaxValue | The number of units of goods purchased.                                                                                                  |
| ***`purchaseCurrency`*** | string | from 1 to 24 symbols   | The name of a currency used for the purchase.                                                                                            |
| ***`purchasePrice`***    | int    | from 1 to int.MaxValue | The price of the purchased item in the specified in-game currency.                                                                       |
| {% endtab %}             |        |                        |                                                                                                                                          |

{% tab title="Unity" %}

```csharp
DTDAnalytics.VirtualCurrencyPayment(purchaseId: "Purchase ID",
                                    purchaseType: "Purchase type",
                                    purchaseAmount: 100,
                                    purchasePrice: 10,
                                    purchaseCurrency: "Purchase currency")
```

In case the item is sold for more than one currency/resource, you need to build a dictionary with all the names and amounts of the currencies/resources.

```csharp
var resources = new Dictionary<string, int>
{
    ["Purchase currency name 1"] = 100,
    ["purchase Currency name 2"] = 10
};
DTDAnalytics.VirtualCurrencyPayment(
    purchaseId: "Purchase ID",
    purchaseType: "Purchase Type",
    purchaseAmount: 100,
    resources: resources);
```

| Parameter                | Type   | Restrictions             | Description                                                                                                                              |
| ------------------------ | ------ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| ***`purchaseId`***       | string | from 1 to 32 symbols     | A unique purchase name or ID. Make sure that the names are always in the same language otherwise they will be listed as different items. |
| ***`purchaseType`***     | string | from 1 to 96 symbols     | The name of a resource group. For example, for “Wood” it can be “Construction materials”.                                                |
| ***`purchaseAmount`***   | int    | from 1 to Int32.MaxValue | The number of units of goods purchased.                                                                                                  |
| ***`purchaseCurrency`*** | string | from 1 to 24 symbols     | The name of a currency used for the purchase.                                                                                            |
| ***`purchasePrice`***    | int    | from 1 to Int32.MaxValue | The price of the purchased item in the specified in-game currency.                                                                       |
| {% endtab %}             |        |                          |                                                                                                                                          |

{% tab title="Web" %}

```javascript
analytics.virtualCurrencyPayment(
                                purchaseId,
                                purchaseType,
                                purchaseAmount, 
                                purchasePrice,
                                purchaseCurrency)
```

In case the item is sold for more than one currency/resource, you need to build a dictionary with all the names and amounts of the currencies/resources.

```javascript
const resources = {
                "Purchase currency name 1": 100,
                "purchase Currency name 2": 10
};
analytics.virtualCurrencyPayment(
                "Purchase ID",
                "Purchase type",
                100, 
                resources)
```

| Parameter                | Type   | Restrictions                        | Description                                                                                                                              |
| ------------------------ | ------ | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| ***`purchaseId`***       | string | from 1 to 32 symbols                | A unique purchase name or ID. Make sure that the names are always in the same language otherwise they will be listed as different items. |
| ***`purchaseType`***     | string | from 1 to 96 symbols                | The name of a resource group. For example, for “Wood” it can be “Construction materials”.                                                |
| ***`purchaseAmount`***   | int    | from 1 to Number.MAX\_SAFE\_INTEGER | The number of units of goods purchased.                                                                                                  |
| ***`purchaseCurrency`*** | string | from 1 to 24 symbols                | The name of a currency used for the purchase.                                                                                            |
| ***`purchasePrice`***    | int    | from 1 to Number.MAX\_SAFE\_INTEGER | The price of the purchased item in the specified in-game currency.                                                                       |
| {% endtab %}             |        |                                     |                                                                                                                                          |

{% tab title="Unreal" %}

![Blueprint](/files/CZOVIjNj5RPeEI9HYJYX)

| Parameter                | Type    | Restrictions             | Description                                                                                                                              |
| ------------------------ | ------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| ***`purchaseId`***       | FString | from 1 to 32 symbols     | A unique purchase name or ID. Make sure that the names are always in the same language otherwise they will be listed as different items. |
| ***`purchaseType`***     | FString | from 1 to 96 symbols     | The name of a resource group. For example, for “Wood” it can be “Construction materials”.                                                |
| ***`purchaseAmount`***   | int32   | from 1 to Int32.MaxValue | The number of units of goods purchased.                                                                                                  |
| ***`purchaseCurrency`*** | FString | from 1 to 24 symbols     | The name of a currency used for the purchase.                                                                                            |
| ***`purchasePrice`***    | int32   | from 1 to Int32.MaxValue | The price of the purchased item in the specified in-game currency.                                                                       |

```cpp
UDTDAnalyticsBPLibrary::VirtualCurrencyPayment("PurchaseId", "PurchaseType", 2, 3, "CurrencyName");
```

In case the item is sold for more than one currency/resource, you need to build a dictionary with all the names (FString) and amounts of the currencies/resources (int32) .

![Blueprint](/files/IFSCrOXIwAKxc2WXWJmF)

| Parameter              | Type                  | Restrictions                                                                 | Restrictions                                                                                                                             |
| ---------------------- | --------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| ***`purchaseId`***     | FString               | from 1 to 32 symbols                                                         | A unique purchase name or ID. Make sure that the names are always in the same language otherwise they will be listed as different items. |
| ***`purchaseType`***   | FString               | from 1 to 96 symbols                                                         | The name of a resource group. For example, for “Wood” it can be “Construction materials”.                                                |
| ***`purchaseAmount`*** | int32                 | from 1 to int32.MaxValue                                                     | The number of units of goods purchased.                                                                                                  |
| ***`resources`***      | TMap\<FString, int32> | <p>FString - from 1 to 24 symbols</p><p>int32 - from 1 to int32.MaxValue</p> | Map with resources.                                                                                                                      |

```cpp
var resources = new Dictionary<string, int>
{
    ["Purchase currency name 1"] = 100,
    ["purchase Currency name 2"] = 10
};
DTDAnalytics.VirtualCurrencyPayment(
    purchaseId: "Purchase ID",
    purchaseType: "Purchase Type",
    purchaseAmount: 100,
    resources: resources);
```

{% endtab %}

{% tab title="Godot" %}

```gdscript
DTDAnalytics.VirtualCurrencyPayment("purchaseID", "purchaseType", 1000, 15, "resource_1")
```

In case the item is sold for more than one currency/resource, you need to build a dictionary with all the names and amounts of the currencies/resources.

```gdscript
var resources = GDDTDInt32Resources.new()
resources.AddValue("resource_1", 700)
resources.AddValue("resource_2", 100)
DTDAnalytics.VirtualCurrencyPaymentWithResources("purchaseID", "purchaseType", 500, resources)
```

| Parameter                | Type   | Restrictions         | Description                                                                                                                              |
| ------------------------ | ------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| ***`purchaseId`***       | String | from 1 to 32 symbols | A unique purchase name or ID. Make sure that the names are always in the same language otherwise they will be listed as different items. |
| ***`purchaseType`***     | String | from 1 to 96 symbols | The name of a resource group. For example, for “Wood” it can be “Construction materials”.                                                |
| ***`purchaseAmount`***   | int    | from 1 to Int32.max  | The number of units of goods purchased.                                                                                                  |
| ***`purchaseCurrency`*** | String | from 1 to 24 symbols | The name of a currency used for the purchase.                                                                                            |
| ***`purchasePrice`***    | int    | from 1 to Int32.max  | The price of the purchased item in the specified in-game currency.                                                                       |
| {% endtab %}             |        |                      |                                                                                                                                          |
| {% endtabs %}            |        |                      |                                                                                                                                          |

## Progression event

This event is for games only. It is worthwhile to integrate this event into a game type project, as specified in the [application settings](/getting-started/adding-an-app-to-the-space). In projects with the “app” type, game events will not be tracked and displayed in the interface, even if they are integrated. You can verify and change the project type in Settings → [General settings](/reports-and-functionality/project-related-reports-and-fuctionality/settings#general-settings).

First of all, the progression event is used in games with short (within one game session) areas/game levels, e.g. match 3 games. You can use the event to collect data on how well or how fast users complete levels, how difficult it is for them, how many resources they gained or spent, and other parameters.

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}
There are two methods in working with progression event:

* **`startProgressionEvent`**
* **`finishProgressionEvent`**

When a player spawns at a location, the following method is called:

```swift
let parameters = DTDStartProgressionEventParameters()
parameters.source = "Source"
parameters.setDifficulty(difficulty: 10)

DTDAnalytics.startProgressionEvent(eventName: "Progression event name", 
                                   parameters: parameters)
```

| Parameter         | Type   | Restrictions         | Description                                                              |
| ----------------- | ------ | -------------------- | ------------------------------------------------------------------------ |
| ***`eventName`*** | string | from 1 to 40 symbols | The name of the event. It is usually the number or the name of the area. |

**`DTDStartProgressionEventParameters`**:

| Parameter          | Type   | Restrictions         | Description                                                                                                     |
| ------------------ | ------ | -------------------- | --------------------------------------------------------------------------------------------------------------- |
| ***`source`***     | string | from 1 to 40 symbols | The name of the previous event used for connecting events together. E.g. a previous area visited by the player. |
| ***`difficulty`*** | int    | from 0 to Int32.max  | An optional difficulty value which is set using the value: **`setDifficulty(difficulty: Int)`**                 |

Once the player completes the location successfully, the following method is called:

```swift
let parameters = DTDFinishProgressionEventParameters()
parameters.successfulCompletion = true
parameters.duration = 100
parameters.spent = ["currency name 1": 1000,
                    "currency name 2": 50]
parameters.earned = ["currency name 2": 100]

DTDAnalytics.finishProgressionEvent(eventName: "Progression event name", 
                                    parameters: parameters)
```

<table><thead><tr><th>Parameter</th><th width="150">Type</th><th>Restrictions</th><th>Description</th></tr></thead><tbody><tr><td><em><strong><code>eventName</code></strong></em></td><td>string</td><td>from 1 to 40 symbols</td><td>The name of the event. It is usually the number or the name of the area. It’s important to use the name that was specified at the area’s opening.</td></tr></tbody></table>

**`DTDFinishProgressionEventParameters`**:

<table><thead><tr><th>Parameter</th><th width="150">Type</th><th>Restrictions</th><th>Description</th></tr></thead><tbody><tr><td><em><strong><code>successfulCompletion</code></strong></em></td><td>bool</td><td>true/false</td><td>The completion event result. ‘<em><strong>True</strong></em>’ if successful, ‘<em><strong>false</strong></em>’ if unsuccessful/lost.</td></tr><tr><td><em><strong><code>duration</code></strong></em></td><td>int</td><td>from 0 to Int64.max</td><td>Time in seconds taken to complete the area. If not specified, it is automatically calculated as the difference between <strong><code>startProgressionEvent</code></strong> and <strong><code>finishProgressionEvent</code></strong> method calls. </td></tr><tr><td><em><strong><code>spent</code></strong></em></td><td>[String: Int]</td><td><p>key - from 1 to 24 symbols</p><p>value - from 1 to Int64.max</p></td><td>Resources consumed during an area completion.</td></tr><tr><td><em><strong><code>earned</code></strong></em></td><td>[String: Int]</td><td><p>key - from 1 to 24 symbols</p><p>value - from 1 to Int64.max</p></td><td>Resources earned during an area completion.</td></tr></tbody></table>
{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}
There are two methods in working with progression event:

* **`startProgressionEvent`**
* **`finishProgressionEvent`**

When a player spawns at a location, the following method is called:

```objectivec
DTDStartProgressionEventParameters * parameters = [[DTDStartProgressionEventParameters alloc] init];
parameters.source = @"Source";
[parameters setDifficultyWithDifficulty:10];

[DTDAnalytics startProgressionEvent:@"Progression event name" withParameters:parameters];
```

| Parameter         | Type     | Restrictions         | Description                                                              |
| ----------------- | -------- | -------------------- | ------------------------------------------------------------------------ |
| ***`eventName`*** | NSString | from 1 to 40 symbols | The name of the event. It is usually the number or the name of the area. |

**`DTDStartProgressionEventParameters`**:

| Parameter          | Type      | Restrictions         | Description                                                                                                     |
| ------------------ | --------- | -------------------- | --------------------------------------------------------------------------------------------------------------- |
| ***`source`***     | NSString  | from 1 to 40 symbols | The name of the previous event used for connecting events together. E.g. a previous area visited by the player. |
| ***`difficulty`*** | NSInteger | from 0 to Int32.max  | An optional difficulty value which is set using the value: **`setDifficulty(difficulty: Int)`**                 |

Once the player completes the location successfully, the following method is called:

```objectivec
DTDFinishProgressionEventParameters *parameters = [[DTDFinishProgressionEventParameters alloc] init];
parameters.successfulCompletion = true;
parameters.duration = 100;
parameters.spent = @{@"currency name 1": @1000,
                     @"currency name 2": @50};
parameters.earned = @{@"currency name 2": @100};

[DTDAnalytics finishProgressionEvent:@"Progression event name" withParameters:parameters];
```

| Parameter         | Type     | Restrictions         | Description                                                                                                                                       |
| ----------------- | -------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`eventName`*** | NSString | from 1 to 40 symbols | The name of the event. It is usually the number or the name of the area. It’s important to use the name that was specified at the area’s opening. |

**`DTDFinishProgressionEventParameters`**:

| Parameter                    | Type                                   | Restrictions                                                        | Description                                                                                                                                                                                       |
| ---------------------------- | -------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`successfulCompletion`*** | BOOL                                   | true/false                                                          | The completion event result. ‘***True***’ if successful, ‘***false***’ if unsuccessful/lost.                                                                                                      |
| ***`duration`***             | NSInteger                              | from 0 to Int64.max                                                 | Time in seconds taken to complete the area. If not specified, it is automatically calculated as the difference between **`startProgressionEvent`** and **`finishProgressionEvent`** method calls. |
| ***`spent`***                | NSDictionary\<NSString \*,NSNumber \*> | <p>key - from 1 to 24 symbols</p><p>value - from 1 to Int64.max</p> | Resources consumed during an area completion.                                                                                                                                                     |
| ***`earned`***               | NSDictionary\<NSString \*,NSNumber \*> | <p>key - from 1 to 24 symbols</p><p>value - from 1 to Int64.max</p> | Resources earned during an area completion.                                                                                                                                                       |
| {% endtab %}                 |                                        |                                                                     |                                                                                                                                                                                                   |

{% tab title="Android (Kotlin)" %}
There are two methods in working with progression event:

* **`startProgressionEvent`**
* **`finishProgressionEvent`**

When a player spawns at a location, the following method is called:

```kotlin
let parameters = DTDStartProgressionEventParameters()
parameters.source = "Source"
parameters.setDifficulty(difficulty = 10)

DTDAnalytics.startProgressionEvent(
    eventName = "Progression event name", 
    parameters = parameters
)
```

| Parameter         | Type   | Restrictions         | Description                                                              |
| ----------------- | ------ | -------------------- | ------------------------------------------------------------------------ |
| ***`eventName`*** | string | from 1 to 40 symbols | The name of the event. It is usually the number or the name of the area. |

**`DTDStartProgressionEventParameters`**:

| Parameter          | Type   | Restrictions         | Description                                                                                                     |
| ------------------ | ------ | -------------------- | --------------------------------------------------------------------------------------------------------------- |
| ***`source`***     | string | from 1 to 40 symbols | The name of the previous event used for connecting events together. E.g. a previous area visited by the player. |
| ***`difficulty`*** | int    | from 0 to Int32.max  | An optional difficulty value which is set using the value: **`setDifficulty(difficulty: Int)`**                 |

Once the player completes the location successfully, the following method is called:

```kotlin
let parameters = DTDFinishProgressionEventParameters()
parameters.successfulCompletion = true
parameters.duration = 100
parameters.spent = ["currency name 1": 1000,
                    "currency name 2": 50]
parameters.earned = ["currency name 2": 100]

DTDAnalytics.finishProgressionEvent(
    eventName = "Progression event name",
    parameters = parameters
)
```

| Parameter         | Type   | Restrictions         | Description                                                                                                                                       |
| ----------------- | ------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`eventName`*** | string | from 1 to 40 symbols | The name of the event. It is usually the number or the name of the area. It’s important to use the name that was specified at the area’s opening. |

**`DTDFinishProgressionEventParameters`**:

| Parameter                    | Type               | Restrictions                                                        | Description                                                                                                                                                                                       |
| ---------------------------- | ------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`successfulCompletion`*** | bool               | true/false                                                          | The completion event result. ‘***True***’ if successful, ‘***false***’ if unsuccessful/lost.                                                                                                      |
| ***`duration`***             | int                | from 0 to Int64.max                                                 | Time in seconds taken to complete the area. If not specified, it is automatically calculated as the difference between **`startProgressionEvent`** and **`finishProgressionEvent`** method calls. |
| ***`spent`***                | Map\<String, Long> | <p>key - from 1 to 24 symbols</p><p>value - from 1 to Int64.max</p> | Resources consumed during an area completion.                                                                                                                                                     |
| ***`earned`***               | Map\<String, Long> | <p>key - from 1 to 24 symbols</p><p>value - from 1 to Int64.max</p> | Resources earned during an area completion.                                                                                                                                                       |
| {% endtab %}                 |                    |                                                                     |                                                                                                                                                                                                   |

{% tab title="Android (Java)" %}
There are two methods in working with progression event:

* **`startProgressionEvent`**
* **`finishProgressionEvent`**

When a player spawns at a location, the following method is called:

```java
DTDStartProgressionEventParameters parameters = new DTDStartProgressionEventParameters();
parameters.setSource("Source");
parameters.setDifficulty(10);
DTDAnalytics.INSTANCE.startProgressionEvent("Progression event name", parameters);
```

| Parameter         | Type   | Restrictions         | Description                                                              |
| ----------------- | ------ | -------------------- | ------------------------------------------------------------------------ |
| ***`eventName`*** | string | from 1 to 40 symbols | The name of the event. It is usually the number or the name of the area. |

**`DTDStartProgressionEventParameters`**:

| Parameter          | Type   | Restrictions         | Description                                                                                                     |
| ------------------ | ------ | -------------------- | --------------------------------------------------------------------------------------------------------------- |
| ***`source`***     | string | from 1 to 40 symbols | The name of the previous event used for connecting events together. E.g. a previous area visited by the player. |
| ***`difficulty`*** | int    | from 0 to Int32.max  | An optional difficulty value which is set using the value: **`setDifficulty(difficulty: Int)`**                 |

Once the player completes the location successfully, the following method is called:

```java
Map<String, Long> spendMap = new HashMap<>();
spendMap.put("currency name 1", 1000L);
spendMap.put("currency name 2", 100L);

Map<String, Long> earnedMap = new HashMap<>();
spendMap.put("currency name 2", 100L);

DTDFinishProgressionEventParameters param = new DTDFinishProgressionEventParameters();
param.setSuccessfulCompletion(true);
param.setDuration(100);
param.setSpent(spendMap);
param.setEarned(earnedMap);

DTDAnalytics.INSTANCE.finishProgressionEvent("Progression event name", param);
```

| Parameter         | Type   | Restrictions         | Description                                                                                                                                       |
| ----------------- | ------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`eventName`*** | string | from 1 to 40 symbols | The name of the event. It is usually the number or the name of the area. It’s important to use the name that was specified at the area’s opening. |

**`DTDFinishProgressionEventParameters`**:

| Parameter                    | Type               | Restrictions                                                        | Description                                                                                                                                                                                       |
| ---------------------------- | ------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`successfulCompletion`*** | bool               | true/false                                                          | The completion event result. ‘***True***’ if successful, ‘***false***’ if unsuccessful/lost.                                                                                                      |
| ***`duration`***             | int                | from 0 to Int64.max                                                 | Time in seconds taken to complete the area. If not specified, it is automatically calculated as the difference between **`startProgressionEvent`** and **`finishProgressionEvent`** method calls. |
| ***`spent`***                | Map\<String, Long> | <p>key - from 1 to 24 symbols</p><p>value - from 1 to Int64.max</p> | Resources consumed during an area completion.                                                                                                                                                     |
| ***`earned`***               | Map\<String, Long> | <p>key - from 1 to 24 symbols</p><p>value - from 1 to Int64.max</p> | Resources earned during an area completion.                                                                                                                                                       |
| {% endtab %}                 |                    |                                                                     |                                                                                                                                                                                                   |

{% tab title=".NET Native + UWP" %}
There are two methods in working with progression event:

* **`StartProgressionEvent`**
* **`FinishProgressionEvent`**

When a player spawns at a location, the following method is called:

```csharp
var parameters = new DTDStartProgressionEventParameters();
parameters.Source = "Source";
parameters.Difficulty = 10;
DTDAnalytics.StartProgressionEvent(
    eventName: "Progression event name",
    parameters: parameters);
```

| Parameter         | Type   | Restrictions         | Description                                                              |
| ----------------- | ------ | -------------------- | ------------------------------------------------------------------------ |
| ***`eventName`*** | string | from 1 to 40 symbols | The name of the event. It is usually the number or the name of the area. |

**`DTDStartProgressionEventParameters`**:

| Parameter          | Type   | Restrictions           | Description                                                                                                     |
| ------------------ | ------ | ---------------------- | --------------------------------------------------------------------------------------------------------------- |
| ***`source`***     | string | from 1 to 40 symbols   | The name of the previous event used for connecting events together. E.g. a previous area visited by the player. |
| ***`difficulty`*** | int    | from 0 to int.MaxValue | An optional difficulty value.                                                                                   |

Once the player completes the location successfully, the following method is called:

```csharp
var parameters = new DTDFinishProgressionEventParameters();
parameters.SuccessfulCompletion = true;
parameters.Duration = 100;
parameters.Spent = new Dictionary<string, long>
{
    ["currency name 1"] = 1000,
    ["currency name 2"] = 50
};
parameters.Earned = new Dictionary<string, long>
{
    ["currency name 2"] = 100
};
DTDAnalytics.FinishProgressionEvent(
    eventName: "Progression event name",
    parameters: parameters);
```

| Parameter         | Type   | Restrictions         | Description                                                                                                                                       |
| ----------------- | ------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`eventName`*** | string | from 1 to 40 symbols | The name of the event. It is usually the number or the name of the area. It’s important to use the name that was specified at the area’s opening. |

**`DTDFinishProgressionEventParameters`**:

| Parameter                    | Type           | Restrictions                                                           | Description                                                                                                                                                                                       |
| ---------------------------- | -------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`successfulCompletion`*** | bool           | true/false                                                             | The completion event result. ‘**`True`**’ if successful, ‘**`false`**’ if unsuccessful/lost.                                                                                                      |
| ***`duration`***             | int            | from 0 to int.MaxValue                                                 | Time in seconds taken to complete the area. If not specified, it is automatically calculated as the difference between **`StartProgressionEvent`** and **`FinishProgressionEvent`** method calls. |
| ***`spent`***                | \[String: Int] | <p>key - from 1 to 24 symbols</p><p>value - From 1 to int.MaxValue</p> | Resources consumed during an area completion.                                                                                                                                                     |
| ***`earned`***               | \[String: Int] | <p>key - from 1 to 24 symbols</p><p>value - From 1 to int.MaxValue</p> | Resources earned during an area completion.                                                                                                                                                       |
| {% endtab %}                 |                |                                                                        |                                                                                                                                                                                                   |

{% tab title="Unity" %}
There are two methods in working with progression event:

* **`StartProgressionEvent`**
* **`FinishProgressionEvent`**

When a player spawns at a location, the following method is called:

```csharp
var parameters = new DTDStartProgressionEventParameters();
parameters.Source = "Source";
parameters.Difficulty = 10;
DTDAnalytics.StartProgressionEvent(
    eventName: "Progression event name",
    parameters: parameters);
```

| Parameter         | Type   | Restrictions         | Description                                                              |
| ----------------- | ------ | -------------------- | ------------------------------------------------------------------------ |
| ***`eventName`*** | string | from 1 to 40 symbols | The name of the event. It is usually the number or the name of the area. |

**`DTDStartProgressionEventParameters`**:

| Parameter          | Type   | Restrictions             | Description                                                                                                     |
| ------------------ | ------ | ------------------------ | --------------------------------------------------------------------------------------------------------------- |
| ***`source`***     | string | from 1 to 40 symbols     | The name of the previous event used for connecting events together. E.g. a previous area visited by the player. |
| ***`difficulty`*** | int    | from 0 to Int32.MaxValue | An optional difficulty value.                                                                                   |

Once the player completes the location successfully, the following method is called:

```csharp
var parameters = new DTDFinishProgressionEventParameters();
parameters.SuccessfulCompletion = true;
parameters.Duration = 100;
parameters.Spent = new Dictionary<string, long>
{
    ["currency name 1"] = 1000,
    ["currency name 2"] = 50
};
parameters.Earned = new Dictionary<string, long>
{
    ["currency name 2"] = 100
};
DTDAnalytics.FinishProgressionEvent(
    eventName: "Progression event name",
    parameters: parameters);
```

| Parameter         | Type   | Restrictions         | Description                                                                                                                                       |
| ----------------- | ------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`eventName`*** | string | from 1 to 40 symbols | The name of the event. It is usually the number or the name of the area. It’s important to use the name that was specified at the area’s opening. |

**`DTDFinishProgressionEventParameters`**:

| Parameter                    | Type           | Restrictions                                                             | Description                                                                                                                                                                                       |
| ---------------------------- | -------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`successfulCompletion`*** | bool           | true/false                                                               | The completion event result. ‘**`True`**’ if successful, ‘**`false`**’ if unsuccessful/lost.                                                                                                      |
| ***`duration`***             | int            | from 0 to Int64.MaxValue                                                 | Time in seconds taken to complete the area. If not specified, it is automatically calculated as the difference between **`StartProgressionEvent`** and **`FinishProgressionEvent`** method calls. |
| ***`spent`***                | \[String: Int] | <p>key - from 1 to 24 symbols</p><p>value - From 1 to Int64.MaxValue</p> | Resources consumed during an area completion.                                                                                                                                                     |
| ***`earned`***               | \[String: Int] | <p>key - from 1 to 24 symbols</p><p>value - From 1 to Int64.MaxValue</p> | Resources earned during an area completion.                                                                                                                                                       |
| {% endtab %}                 |                |                                                                          |                                                                                                                                                                                                   |

{% tab title="Web" %}
There are two methods in working with progression event:

* **`startProgressionEvent`**
* **`finishProgressionEvent`**

When a player spawns at a location, the following method is called:

```javascript
analytics.startProgressionEvent(eventName, parameters)
```

<table><thead><tr><th width="163">Parameter</th><th>Type</th><th>Restrictions</th><th>Description</th></tr></thead><tbody><tr><td><strong><code>eventName</code></strong></td><td>string</td><td>from 1 to 40 symbols</td><td>The name of the event. It is usually the number or the name of the area.</td></tr><tr><td><strong><code>parameters</code></strong></td><td>object</td><td>see below</td><td>Location event parameters.</td></tr></tbody></table>

**`startProgressionEvent`** event parameters:

| Parameter          | Type   | Restrictions                        | Description                                                                                                     |
| ------------------ | ------ | ----------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| ***`source`***     | string | from 1 to 40 symbols                | The name of the previous event used for connecting events together. E.g. a previous area visited by the player. |
| ***`difficulty`*** | int    | from 0 to Number.MAX\_SAFE\_INTEGER | An optional difficulty value.                                                                                   |

Example:

```javascript
analytics.startProgressionEvent("Location 11", {
                difficulty: 10,
                source: "Location 10"
})
```

Once the player completes the location (instead of successfully or not), the following method is called:

```javascript
analytics.finishProgressionEvent("Progression event name", {
                successfulCompletion,
                duration,
                spent,
                earned
})
```

| Parameter          | Type   | Restrictions         | Description                                                                                                                                       |
| ------------------ | ------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`eventName`***  | string | from 1 to 40 symbols | The name of the event. It is usually the number or the name of the area. It’s important to use the name that was specified at the area’s opening. |
| ***`parameters`*** | object | see below            | Location event parameters.                                                                                                                        |

**`startProgressionEvent`** event parameters:

| Parameter                    | Type           | Restrictions                                                                        | Description                                                                                                                                                                                       |
| ---------------------------- | -------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`successfulCompletion`*** | bool           | true/false                                                                          | The completion event result. ‘***True***’ if successful, ‘***false***’ if unsuccessful/lost.                                                                                                      |
| ***`duration`***             | int            | from 0 to Number.MAX\_SAFE\_INTEGER                                                 | Time in seconds taken to complete the area. If not specified, it is automatically calculated as the difference between **`startProgressionEvent`** and **`finishProgressionEvent`** method calls. |
| ***`spent`***                | \[String: Int] | <p>key - from 1 to 24 symbols</p><p>value - from 1 to Number.MAX\_SAFE\_INTEGER</p> | Resources consumed during an area completion.                                                                                                                                                     |
| ***`earned`***               | \[String: Int] | <p>key - from 1 to 24 symbols</p><p>value - from 1 to Number.MAX\_SAFE\_INTEGER</p> | Resources earned during an area completion.                                                                                                                                                       |

Example:

```javascript
analytics.finishProgressionEvent("Location 11", {
                true,
                100,
                spent: {
                                "currency name 1": 1000,
                                "currency name 2": 50 
                },
                earned: {
                                "currency name 2": 100
                }
})
```

{% endtab %}

{% tab title="Unreal" %}
There are two methods in working with progression event:

* **`startProgressionEvent`**
* **`finishProgressionEvent`**

When a player spawns at a location, the following method is called (one of them):

![Blueprint](/files/Xeybli35efMRkCbySqam)

<table><thead><tr><th width="163">Parameter</th><th>Type</th><th>Restrictions</th><th>Description</th></tr></thead><tbody><tr><td><em><strong><code>eventName</code></strong></em></td><td>FString</td><td>from 1 to 72 symbols</td><td>Progression event name.</td></tr></tbody></table>

```cpp
UDTDAnalyticsBPLibrary::StartProgressionEvent("EventName");
```

Start progression event with parameters:

![Blueprint](/files/8248t2LXXXm3MzMwMx70)

<table><thead><tr><th width="163">Parameter</th><th>Type</th><th>Restrictions</th><th>Description</th></tr></thead><tbody><tr><td><em><strong><code>eventName</code></strong></em></td><td>FString</td><td>from 1 to 40 symbols</td><td>The name of the event. It is usually the number or the name of the area.</td></tr><tr><td><em><strong><code>params</code></strong></em></td><td>FDTDStartProgressionEventParams</td><td>see below</td><td>Start progression event parameters.</td></tr></tbody></table>

**FDTDStartProgressionEventParams**:

| Parameter          | Type    | Restrictions             | Description                                                                                                     |
| ------------------ | ------- | ------------------------ | --------------------------------------------------------------------------------------------------------------- |
| ***`source`***     | FString | from 1 to 40 symbols     | The name of the previous event used for connecting events together. E.g. a previous area visited by the player. |
| ***`difficulty`*** | int32   | from 0 to int32.MaxValue | An optional difficulty value.                                                                                   |

Example:

```cpp
StartProgressionEventParams params;
params.Difficulty = 3;
params.Source = "Source";
UDTDAnalyticsBPLibrary::StartProgressionEventWithParams("EventName", params);
```

Once the player completes the location (instead of successfully or not), the following method is called (one of them):

![Blueprint](/files/ZTG8B5JCrtSmvGX2ieWm)

<table><thead><tr><th width="163">Parameter</th><th>Type</th><th>Restrictions</th><th>Description</th></tr></thead><tbody><tr><td><em><strong><code>eventName</code></strong></em></td><td>FString</td><td>from 1 to 72 symbols</td><td>Progression event name.</td></tr></tbody></table>

```cpp
UDTDAnalyticsBPLibrary::FinishProgressionEvent("EventName");window.devtodev.finishProgressionEvent("Progression event name", {
```

Finish progression event with parameters:

<br>

![Blueprint](/files/yFWc0TGJnZYuZOe3KqJ6)

| Parameter          | Type                             | Restrictions         | Description                                                                                                                                       |
| ------------------ | -------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`eventName`***  | FString                          | from 1 to 40 symbols | The name of the event. It is usually the number or the name of the area. It’s important to use the name that was specified at the area’s opening. |
| ***`parameters`*** | FDTDFinishProgressionEventParams | see below            | Finish progression event parameters.                                                                                                              |

**FDTDFinishProgressionEventParameters**:TMap\<FString, int64>

| Parameter                    | Type                  | Restrictions                                                             | Description                                                                                                                                                                                       |
| ---------------------------- | --------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`successfulCompletion`*** | bool                  | true/false                                                               | The completion event result. ‘***True***’ if successful, ‘***false***’ if unsuccessful/lost.                                                                                                      |
| ***`duration`***             | int32                 | from 0 to int32.MaxValue                                                 | Time in seconds taken to complete the area. If not specified, it is automatically calculated as the difference between **`startProgressionEvent`** and **`finishProgressionEvent`** method calls. |
| ***`spent`***                | TMap\<FString, int64> | <p>key - from 1 to 24 symbols</p><p>value - from 0 to int64.MaxValue</p> | Resources consumed during an area completion.                                                                                                                                                     |
| ***`earned`***               | TMap\<FString, int64> | <p>key - from 1 to 24 symbols</p><p>value - from 0 to int64.MaxValue</p> | Resources earned during an area completion.                                                                                                                                                       |

Example:

```cpp
FDTDFinishProgressionEventParams params;
params.Duration = 200;
params.SuccessfulCompletion = true;
params.Earned.Add("CurrencyName1", 1);
params.Spent.Add("CurrencyName2", 2);
UDTDAnalyticsBPLibrary::FinishProgressionEventWithParams("EventName", params);
```

{% endtab %}

{% tab title="Godot" %}
There are two methods in working with progression event:

* **`StartProgressionEvent`**
* **`FinishProgressionEvent`**

When a player spawns at a location, the following method is called:

```gdscript
var params = GDDTDStartProgressionEventParams.new()
params.SetDifficulty(10)
params.SetSource("source_1")

DTDAnalytics.StartProgressionEventWithParams("ProgressionEventWithParams", params)
```

| Parameter         | Type   | Restrictions         | Description                                                              |
| ----------------- | ------ | -------------------- | ------------------------------------------------------------------------ |
| ***`eventName`*** | String | from 1 to 40 symbols | The name of the event. It is usually the number or the name of the area. |

**`GDDTDStartProgressionEventParameters`**:

| Parameter          | Type   | Restrictions         | Description                                                                                                     |
| ------------------ | ------ | -------------------- | --------------------------------------------------------------------------------------------------------------- |
| ***`source`***     | String | from 1 to 40 symbols | The name of the previous event used for connecting events together. E.g. a previous area visited by the player. |
| ***`difficulty`*** | int    | from 0 to Int32.max  | An optional difficulty value which is set using the value: **`SetDifficulty()`**                                |

Once the player completes the location successfully, the following method is called:

```gdscript
var earnedResources = GDDTDInt64Resources.new()
earnedResources.AddValue("resource_1", 200)
earnedResources.AddValue("resource_2", 1500)
	
var spentResources = GDDTDInt64Resources.new()
spentResources.AddValue("resource_1", 100)
spentResources.AddValue("resource_2", 600)
	
var params = GDDTDFinishProgressionEventParams.new()
params.SetSuccessfulCompletion(true)
params.SetEarnedResources(earnedResources)
params.SetSpentResources(spentResources)
params.SetDuration(2000)
DTDAnalytics.FinishProgressionEventWithParams("ProgressionEventWithParams", params)
```

<table><thead><tr><th>Parameter</th><th width="150">Type</th><th>Restrictions</th><th>Description</th></tr></thead><tbody><tr><td><em><strong><code>eventName</code></strong></em></td><td>string</td><td>from 1 to 40 symbols</td><td>The name of the event. It is usually the number or the name of the area. It’s important to use the name that was specified at the area’s opening.</td></tr></tbody></table>

**`GDDTDFinishProgressionEventParameters`**:

<table><thead><tr><th>Parameter</th><th width="132">Type</th><th>Restrictions</th><th>Description</th></tr></thead><tbody><tr><td><em><strong><code>successfulCompletion</code></strong></em></td><td>bool</td><td>true/false</td><td>The completion event result. ‘<em><strong>True</strong></em>’ if successful, ‘<em><strong>false</strong></em>’ if unsuccessful/lost.</td></tr><tr><td><em><strong><code>duration</code></strong></em></td><td>int</td><td>from 0 to Int64.max</td><td>Time in seconds taken to complete the area. If not specified, it is automatically calculated as the difference between <strong><code>startProgressionEvent</code></strong> and <strong><code>finishProgressionEvent</code></strong> method calls. </td></tr><tr><td><em><strong><code>spent</code></strong></em></td><td>GDDTDInt64Resources</td><td><p>key - from 1 to 24 symbols</p><p>value - from 1 to Int64.max</p></td><td>Resources consumed during an area completion.</td></tr><tr><td><em><strong><code>earned</code></strong></em></td><td>GDDTDInt64Resources</td><td><p>key - from 1 to 24 symbols</p><p>value - from 1 to Int64.max</p></td><td>Resources earned during an area completion.</td></tr></tbody></table>
{% endtab %}
{% endtabs %}

{% hint style="info" %}
The user can only be in one area at a time. When moving to another area (including nested ones), the previous location must be completed. Information about events that have not been completed by calling the **`finishProgressionEvent`** method during a game session (the **`finishProgressionEvent`** method call is not integrated, or the user uses the cached app, or the app has crashed) is not included in the statistics.

If you want to use this event to track actions that take more than one game session, you can prepare the required data and call both methods when the action is completed (successfully or unsuccessfully). For example, you can use this event to track the main questline.
{% endhint %}


# Secondary methods

## Ad impression

The event is used for individual tracking of ad revenue on user devices. The method is used if there are CPI data available on the client device (they can be obtained from the ad network SDK).

{% hint style="info" %}
Do not apply this method if you use ad networks that utilize the server-server protocol for sending ad revenue data (ironSource, AppLovin MAX, and Fyber networks) and you already set up this method of data collection because if you use both data sources, your revenue data may be duplicated.
{% endhint %}

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

| Parameter       | Type    | Restrictions                              | Description                                           |
| --------------- | ------- | ----------------------------------------- | ----------------------------------------------------- |
| **`network`**   | String  | from 1 to 100 symbols                     | Name of the ad network responsible for the impression |
| **`revenue`**   | Double  | from 0,0 to Double.max                    | Reward for banner display in USD                      |
| **`placement`** | String? | <p>from 1 to 100 symbols,<br>optional</p> | Banner placement                                      |
| **`unit`**      | String? | <p>from 1 to 100 symbols,<br>optional</p> | Banner name                                           |

```swift
DTDAnalytics.adImpression(network: "Network name",
                          revenue: 0.15,
                          placement: "Placement of the banner",
                          unit: "Banner title")
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

| Parameter       | Type                    | Restrictions                              | Description                                           |
| --------------- | ----------------------- | ----------------------------------------- | ----------------------------------------------------- |
| **`network`**   | NSString                | from 1 to 100 symbols                     | Name of the ad network responsible for the impression |
| **`revenue`**   | double                  | from 0,0 to Double.max                    | Reward for banner display in USD                      |
| **`placement`** | NSString **\_Nullable** | <p>from 1 to 100 symbols,<br>optional</p> | Banner placement                                      |
| **`unit`**      | NSString **\_Nullable** | <p>from 1 to 100 symbols,<br>optional</p> | Banner name                                           |

```objectivec
[DTDAnalytics adImpressionWithNetwork:@"Network name"
                              revenue:0.15f
                            placement:@"Placement of the banner"
                                unit:@"Banner title"];
```

{% endtab %}

{% tab title="Android (Kotlin)" %}

| Parameter       | Type    | Restrictions                              | Description                                           |
| --------------- | ------- | ----------------------------------------- | ----------------------------------------------------- |
| **`network`**   | String  | from 1 to 100 symbols                     | Name of the ad network responsible for the impression |
| **`revenue`**   | Double  | from 0,0 to Double.max                    | Reward for banner display in USD                      |
| **`placement`** | String? | <p>from 1 to 100 symbols,<br>optional</p> | Banner placement                                      |
| **`unit`**      | String? | <p>from 1 to 100 symbols,<br>optional</p> | Banner name                                           |

```kotlin
DTDAnalytics.adImpression(
    network = "Network name",
    revenue = 0.45,
    placement = "Placement of the banner",
    unit = "Banner title"
)
```

{% endtab %}

{% tab title="Android (Java)" %}

| Parameter       | Type    | Restrictions                              | Description                                           |
| --------------- | ------- | ----------------------------------------- | ----------------------------------------------------- |
| **`network`**   | String  | from 1 to 100 symbols                     | Name of the ad network responsible for the impression |
| **`revenue`**   | Double  | from 0,0 to Double.max                    | Reward for banner display in USD                      |
| **`placement`** | String? | <p>from 1 to 100 symbols,<br>optional</p> | Banner placement                                      |
| **`unit`**      | String? | <p>from 1 to 100 symbols,<br>optional</p> | Banner name                                           |

```java
DTDAnalytics.INSTANCE.adImpression(
        "Network name",
        0.45,
        "Placement of the banner",
        "Banner title"
);
```

{% endtab %}

{% tab title=".NET Native + UWP" %}

| Parameter       | Type    | Restrictions                              | Description                                           |
| --------------- | ------- | ----------------------------------------- | ----------------------------------------------------- |
| **`network`**   | String  | from 1 to 100 symbols                     | Name of the ad network responsible for the impression |
| **`revenue`**   | Double  | from 0,0 to Double.max                    | Reward for banner display in USD                      |
| **`placement`** | String? | <p>from 1 to 100 symbols,<br>optional</p> | Banner placement                                      |
| **`unit`**      | String? | <p>from 1 to 100 symbols,<br>optional</p> | Banner name                                           |

```csharp
var network = "Network name";
var revenue = 0.15;
var placement = "Placement of the banner";
var unit = "Banner title";
DTDAnalytics.AdImpression(network, revenue, placement, unit);
```

{% endtab %}

{% tab title="Unity" %}

| Parameter       | Type    | Restrictions                              | Description                                           |
| --------------- | ------- | ----------------------------------------- | ----------------------------------------------------- |
| **`network`**   | String  | from 1 to 100 symbols                     | Name of the ad network responsible for the impression |
| **`revenue`**   | Double  | from 0,0 to Double.max                    | Reward for banner display in USD                      |
| **`placement`** | String? | <p>from 1 to 100 symbols,<br>optional</p> | Banner placement                                      |
| **`unit`**      | String? | <p>from 1 to 100 symbols,<br>optional</p> | Banner name                                           |

```csharp
var network = "Network name";
var revenue = 0.15;
var placement = "Placement of the banner";
var unit = "Banner title";
DTDAnalytics.AdImpression(network, revenue, placement, unit);
```

{% endtab %}

{% tab title="Unreal" %}
![Blueprint](/files/lRVzWiKBcRSYBUyuhguG)

| **Parameter** | **Type** | **Restrictions**           | **Description**                                           |
| ------------- | -------- | -------------------------- | --------------------------------------------------------- |
| socialNetwork | FString  | from 1 to 100 symbols      | The name of the ad network that delivered the impression. |
| revenue       | float    | form 0.0 to float.MaxValue | Reward for displaying a banner in USD.                    |
| placement     | FString  | from 1 to 100 symbols      | Placement of the banner.                                  |
| unit          | FString  | from 1 to 100 symbols      | Banner title.                                             |

```cpp
UDTDAnalyticsBPLibrary::AdImpression("NetworkName", 0.36, "BannerPlacement", "BannerTitle");
```

{% endtab %}

{% tab title="Godot" %}

| Parameter       | Type   | Restrictions                              | Description                                           |
| --------------- | ------ | ----------------------------------------- | ----------------------------------------------------- |
| **`network`**   | String | from 1 to 100 symbols                     | Name of the ad network responsible for the impression |
| **`revenue`**   | Float  | from 0,0 to Double.max                    | Reward for banner display in USD                      |
| **`placement`** | String | <p>from 1 to 100 symbols,<br>optional</p> | Banner placement                                      |
| **`unit`**      | String | <p>from 1 to 100 symbols,<br>optional</p> | Banner name                                           |

```gdscript
DTDAnalytics.AdImpression("Network name", 0.15, "Placement of the banner", "Banner title")
```

{% endtab %}
{% endtabs %}

## Connecting to social networks

The event is used to track connections to social media channels.

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}
Use the following constants to specify a social network:

***`.facebook, .vkontakte , .twitter, .googleplus, .whatsapp, .viber, .evernote, .googlemail, .linkedin, .pinterest, .qzone, .reddit, .renren, .tumblr`***

Or create an object with the desired social media name.

```swift
let network = DTDSocialNetwork(name: "NetworkName")
DTDAnalytics.socialNetworkConnect(socialNetwork: network)
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}
Use the following constants to specify a social network:

***`.facebook, .vkontakte , .twitter, .googleplus, .whatsapp, .viber, .evernote, .googlemail, .linkedin, .pinterest, .qzone, .reddit, .renren, .tumblr`***

Or create an object with the desired social media name.

```objectivec
DTDSocialNetwork *network = [[DTDSocialNetwork alloc] initWithName:@"NetworkName"];
[DTDAnalytics socialNetworkConnect:network];
```

{% endtab %}

{% tab title="Android (Kotlin)" %}
Use the following constants to specify a social network:

***`DTDSocialNetwork.facebook, DTDSocialNetwork.vkontakte , DTDSocialNetwork.twitter, DTDSocialNetwork.googleplus, DTDSocialNetwork.whatsapp, DTDSocialNetwork.viber, DTDSocialNetwork.evernote, DTDSocialNetwork.googlemail, DTDSocialNetwork.linkedin, DTDSocialNetwork.pinterest, DTDSocialNetwork.qzone, DTDSocialNetwork.reddit, DTDSocialNetwork.renren, DTDSocialNetwork.tumblr`***

Or create an object with the desired social media name.

```kotlin
DTDAnalytics.socialNetworkPost(
    socialNetwork = DTDSocialNetwork.facebook
)
```

{% endtab %}

{% tab title="Android (Java)" %}
Use the following constants to specify a social network:

**`DTDSocialNetwork.Companion.getFacebook(), DTDSocialNetwork.Companion.getVkontakte(), DTDSocialNetwork.Companion.getTwitter(), DTDSocialNetwork.Companion.getGoogleplus(), DTDSocialNetwork.Companion.Whatsapp(), DTDSocialNetwork.Companion.getViber(), DTDSocialNetwork.Companion.getEvernote(), DTDSocialNetwork.Companion.getGooglemail(), DTDSocialNetwork.Companion.getLinkedin(), DTDSocialNetwork.Companion.getPinterest(), DTDSocialNetwork.Companion.getQzone(), DTDSocialNetwork.Companion.getReddit(), DTDSocialNetwork.Companion.getRenren(), DTDSocialNetwork.Companion.getTumblr()`**

Or create an object with the desired social media name.

```java
DTDAnalytics.INSTANCE.socialNetworkConnect(DTDSocialNetwork.Companion.getFacebook());
```

{% endtab %}

{% tab title=".NET Native + UWP" %}
Use the following constants to specify a social network:

***`DTDSocialNetwork.facebook, DTDSocialNetwork.vkontakte , DTDSocialNetwork.twitter, DTDSocialNetwork.googleplus, DTDSocialNetwork.whatsapp, DTDSocialNetwork.viber, DTDSocialNetwork.evernote, DTDSocialNetwork.googlemail, DTDSocialNetwork.linkedin, DTDSocialNetwork.pinterest, DTDSocialNetwork.qzone, DTDSocialNetwork.reddit, DTDSocialNetwork.renren, DTDSocialNetwork.tumblr`***

Or create an object with the desired social media name:

```csharp
var network = new DTDSocialNetwork(name: "NetworkName");
DTDAnalytics.SocialNetworkConnect(socialNetwork: network);
```

{% endtab %}

{% tab title="Unity" %}
Use the following constants to specify a social network:

***`DTDSocialNetwork.facebook, DTDSocialNetwork.vkontakte , DTDSocialNetwork.twitter, DTDSocialNetwork.googleplus, DTDSocialNetwork.whatsapp, DTDSocialNetwork.viber, DTDSocialNetwork.evernote, DTDSocialNetwork.googlemail, DTDSocialNetwork.linkedin, DTDSocialNetwork.pinterest, DTDSocialNetwork.qzone, DTDSocialNetwork.reddit, DTDSocialNetwork.renren, DTDSocialNetwork.tumblr`***

Or create an object with the desired social media name:

```csharp
var network = new DTDSocialNetwork(name: "NetworkName");
DTDAnalytics.SocialNetworkConnect(socialNetwork: network);
```

{% endtab %}

{% tab title="Web" %}

```javascript
analytics.socialNetworkConnect('NetworkName')
```

{% endtab %}

{% tab title="Unreal" %}
![Blueprint](/files/VGclUbaMaao8Hr0Ey5Yx)

| **Argument**  | **Type**          | **Description**            |
| ------------- | ----------------- | -------------------------- |
| socialNetwork | EDTDSocialNetwork | Predefined social network. |

```cpp
UDTDAnalyticsBPLibrary::SocialNetworkConnect(EDTDSocialNetwork::Linkedin);
```

Or use special method for custom social network:

![Blueprint](/files/4wL3bh5d1LwkBsqVLVG8)

```cpp
UDTDAnalyticsBPLibrary::SocialNetworkConnectCustom("SocialNetworkName");
```

| **Argument**  | **Type** | **Description**        |
| ------------- | -------- | ---------------------- |
| socialNetwork | FString  | Custom social network. |
| {% endtab %}  |          |                        |

{% tab title="Godot" %}
Use the following constants to specify a social network:

***`.facebook, .vkontakte , .twitter, .googleplus, .whatsapp, .viber, .evernote, .googlemail, .linkedin, .pinterest, .qzone, .reddit, .renren, .tumblr`***

```gdscript
DTDAnalytics.SocialNetworkConnect(GDDTDSocialNetwork.Facebook())
```

Or create an object with the desired social media name.

```gdscript
let network = GDDTDSocialNetwork(name: "NetworkName")
DTDAnalytics.SocialNetworkConnect(network)
```

{% endtab %}
{% endtabs %}

## Posting to social networks

Track social media posts and analyze their effectiveness and virality. Pass the event after the post has been approved by social media.

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
DTDAnalytics.socialNetworkPost(socialNetwork: .facebook, 
                                      reason: "New level reached")
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

```objectivec
[DTDAnalytics socialNetworkPost:DTDSocialNetwork.facebook withReason:@"New level reached"];
```

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
DTDAnalytics.socialNetworkPost(
    socialNetwork = DTDSocialNetwork.facebook,
    reason = "New level reached"
)
```

{% endtab %}

{% tab title="Android (Java)" %}

```java
 DTDAnalytics.INSTANCE.socialNetworkPost(
        DTDSocialNetwork.Companion.getFacebook(),
        "New level reached"
)
```

{% endtab %}

{% tab title=".NET Native + UWP" %}

```csharp
DTDAnalytics.SocialNetworkPost(
    socialNetwork: DTDSocialNetwork.facebook,
    reason: "New level reached");
```

{% endtab %}

{% tab title="Unity" %}

```csharp
DTDAnalytics.SocialNetworkPost(
    socialNetwork: DTDSocialNetwork.Facebook,
    reason: "New level reached");
```

{% endtab %}

{% tab title="Web" %}

```javascript
analytics.socialNetworkPost("NetworkName", "New level reached")
```

{% endtab %}

{% tab title="Unreal" %}
![Blueprint](/files/HOP3uHhtXGaF1ABO13CB)

| **Argument**  | **Type**          | **Description**            |
| ------------- | ----------------- | -------------------------- |
| socialNetwork | EDTDSocialNetwork | Predefined social network. |

```cpp
UDTDAnalyticsBPLibrary::SocialNetworkPost(EDTDSocialNetwork::Linkedin, "PostReason");
```

![Blueprint](/files/ekUEhtQhTDa3nsqpUfip)

| **Argument**  | **Type** | **Description**        |
| ------------- | -------- | ---------------------- |
| socialNetwork | FString  | Custom social network. |

```cpp
UDTDAnalyticsBPLibrary::SocialNetworkPostCustom("SocialNetworkName", "PostReason");
```

{% endtab %}

{% tab title="Godot" %}

```gdscript
DTDAnalytics.SocialNetworkPost(GDDTDSocialNetwork.Facebook(), "New level reached")
```

{% endtab %}
{% endtabs %}

## Referrer

If you have referral information, you can pass it using the following method:

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
let referrerData = [DTDReferralProperty.source: "AdWords",
                    DTDReferralProperty.medium: "CPI",
                    DTDReferralProperty.content: "Snow Boots",
                    DTDReferralProperty.campaign: "Warm Snow Boots",
                    DTDReferralProperty.term: "shoes+boots"]
                    
DTDAnalytics.referrer(utmData: referrerData)
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

```objectivec
NSDictionary <DTDReferralProperty *, NSString *> * referrerData = @{
  DTDReferralProperty.source: @"AdWords",
  DTDReferralProperty.medium: @"CPI",
  DTDReferralProperty.content: @"Snow Boots",
  DTDReferralProperty.campaign: @"Warm Snow Boots",
  DTDReferralProperty.term: @"shoes+boots",
};
[DTDAnalytics referrer:referrerData];
```

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
enum class DTDReferralProperty {
    Source,
    Campaign,
    Content,
    Medium,
    Term;
}

val referrer = mapOf(
        DTDReferralProperty.Medium to "some value",
        DTDReferralProperty.Campaign to "some value"
)    
DTDAnalytics.referrer(utmData = referrer)
```

{% endtab %}

{% tab title="Android (Java)" %}

```java
public final enum class DTDReferralProperty {
    Source,
    Campaign,
    Content,
    Medium,
    Term;
}

Map<DTDReferralProperty, String> propertyStringHashMap = new HashMap<>();
propertyStringHashMap.put(DTDReferralProperty.Medium, "some value");
propertyStringHashMap.put(DTDReferralProperty.Campaign, "some value");
DTDAnalytics.INSTANCE.referrer(propertyStringHashMap);
```

{% endtab %}

{% tab title=".NET Native + UWP" %}

```csharp
var referrer = new Dictionary<DTDReferralProperty, string>
{
    [DTDReferralProperty.Medium] = "some value",
    [DTDReferralProperty.Campaign] = "some value"
};
DTDAnalytics.Referrer(referrer: referrer);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
var referrer = new Dictionary<DTDReferralProperty, string>
{
    [DTDReferralProperty.Medium] = "some value",
    [DTDReferralProperty.Campaign] = "some value"
};
DTDAnalytics.Referrer(referrer: referrer);
```

{% endtab %}

{% tab title="Web" %}

```javascript
const referrer = {
    source: "some source",
    term: "some term",
    medium: "some medium",
    source: "some source",
    content: "some content",
    campaign: "some campaign",
};
analytics.referrer(referrer)
```

{% endtab %}

{% tab title="Unreal" %}
![Blueprint](/files/iAeFbPNwj8LQw5UhgDC9)

| **Argument** | **Type**                             | **Description** |
| ------------ | ------------------------------------ | --------------- |
| utmData      | TMap\<EDTDReferralProperty, FString> | UTM data.       |

```cpp
TMap<EDTDReferralProperty, FString> referrer;
referrer.Add(EDTDReferralProperty::Source, "Source");
referrer.Add(EDTDReferralProperty::Medium, "Medium ");
referrer.Add(EDTDReferralProperty::Content, "Content ");
referrer.Add(EDTDReferralProperty::Campaign, "Campaign ");
referrer.Add(EDTDReferralProperty::Term, "Term ");
UDTDAnalyticsBPLibrary::Referrer(referrer);
```

{% endtab %}

{% tab title="Godot" %}

```gdscript
var reffer = GDDTDReferralProperty.new()
reffer.AddCampaign("Warm Snow Boots")
reffer.AddContent("Snow Boots")
reffer.AddMedium("CPI")
reffer.AddSource("AdWords")
reffer.AddTerm("shoes+boots")

DTDAnalytics.Referrer(reffer)
```

{% endtab %}
{% endtabs %}

## Force dispatch of accumulated events

To send an event packet before it is full (10 events, by default) or before the end of the period of its formation (2 minutes, by default), you can use immediate dispatch.

{% hint style="info" %}
We don’t recommend using this method unless absolutely necessary!\
When the Real payment event is created, the forced dispatch of the packet occurs automatically.
{% endhint %}

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
DTDAnalytics.sendBufferedEvents()
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

```objectivec
[DTDAnalytics sendBufferedEvents];
```

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
DTDAnalytics.sendBufferedEvents()
```

{% endtab %}

{% tab title="Android (Java)" %}

```java
DTDAnalytics.INSTANCE.sendBufferedEvents();
```

{% endtab %}

{% tab title=".NET Native + UWP" %}

```csharp
DTDAnalytics.SendBufferedEvents();
```

{% endtab %}

{% tab title="Unity" %}

```csharp
DTDAnalytics.SendBufferedEvents();
```

{% endtab %}

{% tab title="Web" %}

```javascript
analytics.sendBufferedEvents()
```

{% endtab %}

{% tab title="Unreal" %}
![Blueprint](/files/rgXBkfkKXZamisQRoafI)

```cpp
UDTDAnalyticsBPLibrary::SendBufferedEvents();
```

{% endtab %}

{% tab title="Godot" %}

```gdscript
DTDAnalytics.SendBufferedEvents()
```

{% endtab %}
{% endtabs %}

## Setters & Getters

{% hint style="info" %}
When working with getters you should take into account that the new devtodev SDK is completely asynchronous. The execution result must be processed within the **`completionHandler`**.

All set and get methods need to be called only after the initialization of the SDK.

It is also worth remembering that the SDK will call the callback in background queues, so we recommend that you transfer the processing of return values to the queue you need.
{% endhint %}

For example:

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
DispatchQueue.main.async{}
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

```objectivec
dispatch_async(dispatch_get_main_queue(), ^{ });
```

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
Handler(Looper.getMainLooper()).post{}
```

{% endtab %}

{% tab title="Android (Java)" %}

```java
ContextCompat.getMainExecutor(context).execute(() -> {
    // This is where your UI code goes.
});
```

{% endtab %}

{% tab title=".NET Native + UWP" %}

```csharp
var result = await DTDAnalytics.GetUserId();
```

{% endtab %}

{% tab title="Unity" %}

```csharp
DTDAnalytics.GetUserId( id => {
  //your code
});
```

{% endtab %}

{% tab title="Unreal" %}
![Blueprint](/files/YpG7pxSVEExb73SWBjaM)

| **Argument** | **Type**                                                                                 | **Description** |
| ------------ | ---------------------------------------------------------------------------------------- | --------------- |
| onResult     | <ul><li>FAnalyticsDynamicGetterStringDelegate</li><li>FDTDGetterStringDelegate</li></ul> | Callback.       |

```cpp
auto onResult = new FDTDGetterStringDelegate();
onResult->BindLambda([](const FString& value)
{
	// Your code...
});
UDTDAnalyticsBPLibrary::GetUserId(*onResult);
```

{% endtab %}
{% endtabs %}

### Setting User Tracking Status (GDPR) <a href="#setting-user-tracking-status-gdpr-hardbreak-settrackingavailability" id="setting-user-tracking-status-gdpr-hardbreak-settrackingavailability"></a>

This method denies/allows tracking of user data and also implements the right to be forgotten in accordance with the requirements of the GDPR.

When this method is called with the ***`false`*** value, the SDK sends a command to the server to **delete all personal user data** that was collected by devtodev in this application, blocking further user data collection.&#x20;

The user will remain in the devtodev system only as an impersonal unit in the previously aggregated metrics.

If it is set to ***`true`***, tracking can be enabled again. In this case, the user will be considered new.

To enable/disable user tracking by the devtodev system. Bool type.

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
DTDAnalytics.setTrackingAvailability(value: true)
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

```objectivec
[DTDAnalytics trackingAvailability:true];
```

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
DTDAnalytics.setTrackingAvailability(value = true)
```

{% endtab %}

{% tab title="Android (Java)" %}

```java
DTDAnalytics.INSTANCE.setTrackingAvailability(true);
```

{% endtab %}

{% tab title=".NET Native + UWP" %}

```csharp
DTDAnalytics.SetTrackingAvailability(trackingValue: true);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
DTDAnalytics.SetTrackingAvailability(trackingValue: true);
```

{% endtab %}

{% tab title="Web" %}

```javascript
analytics.setTrackingAvailability(true)
```

{% endtab %}

{% tab title="Unreal" %}
![Blueprint](/files/sEGphe6VJz5uukdJJg4S)

```cpp
UDTDAnalyticsBPLibrary::SetTrackingAvailability(true);
```

{% endtab %}

{% tab title="Godot" %}

```gdscript
DTDAnalytics.SetTrackingAvailability(true)
```

{% endtab %}
{% endtabs %}

### Getting device ID <a href="#getting-device-id-hardbreak-getdeviceid" id="getting-device-id-hardbreak-getdeviceid"></a>

Get device ID. String type.

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
DTDAnalytics.getDeviceId { deviceId in
  // your code
}
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

```objectivec
[DTDAnalytics deviceIdHandler:^(NSString * _Nonnull deviceId) {
  // your code
}];
```

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
DTDAnalytics.getDeviceId { deviceId ->
  // your code
}
```

{% endtab %}

{% tab title="Android (Java)" %}

```java
DTDAnalytics.INSTANCE.getDeviceId(deviceId ->
        // your code
        null
);
```

{% endtab %}

{% tab title=".NET Native + UWP" %}

```csharp
var devideId = await DTDAnalytics.GetDeviceId();
```

{% endtab %}

{% tab title="Unity" %}

```csharp
DTDAnalytics.GetDeviceId( id => {
  //your code
});
```

{% endtab %}

{% tab title="Web" %}

```javascript
const devideId = analytics.getDeviceId()
```

{% endtab %}

{% tab title="Unreal" %}
![Blueprint](/files/y6d2i6UAyzi8ck1bf5Z3)

| **Argument** | **Type**                                                                                 | **Description** |
| ------------ | ---------------------------------------------------------------------------------------- | --------------- |
| onResult     | <ul><li>FAnalyticsDynamicGetterStringDelegate</li><li>FDTDGetterStringDelegate</li></ul> | Callback.       |

```cpp
auto onResult = new FDTDGetterStringDelegate();
onResult->BindLambda([](const FString& value)
{
	// Your code...
});
UDTDAnalyticsBPLibrary::GetDeviceId(*onResult);
```

{% endtab %}

{% tab title="Godot" %}

```gdscript
DTDAnalytics.GetDeviceId(getDeviceHandler)

func getDeviceHandler(deviceId: String):
    print(deviceId)
```

{% endtab %}
{% endtabs %}

### Getting the devtodev SDK version <a href="#getting-the-devtodev-sdk-version-hardbreak-getsdkversion" id="getting-the-devtodev-sdk-version-hardbreak-getsdkversion"></a>

Get the version of the integrated devtodev SDK. String type.&#x20;

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
DTDAnalytics.getSDKVersion { sdkVersion in
  // your code
}
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

```objectivec
[DTDAnalytics sdkVersionHandler:^(NSString * _Nonnull sdkVersion) {
  // your code
}];
```

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
DTDAnalytics.getSDKVersion { sdkVersion ->
  // your code
}
```

{% endtab %}

{% tab title="Android (Java)" %}

```java
DTDAnalytics.INSTANCE.getSdkVersion ( sdkVersion ->
         // your code
         null
);
```

{% endtab %}

{% tab title=".NET Native + UWP" %}

```csharp
var sdkVersion = DTDAnalytics.GetSdkVersion();
```

{% endtab %}

{% tab title="Unity" %}

```
DTDAnalytics.GetSdkVersion( version => {
  //your code
});
```

{% endtab %}

{% tab title="Web" %}

```javascript
const sdkVersion = analytics.getSDKVersion()
```

{% endtab %}

{% tab title="Unreal" %}
![Blueprint](/files/p6bkPLQYi1yHYUt1JBYe)

| **Argument** | **Type**                                                                                 | **Description** |
| ------------ | ---------------------------------------------------------------------------------------- | --------------- |
| onResult     | <ul><li>FAnalyticsDynamicGetterStringDelegate</li><li>FDTDGetterStringDelegate</li></ul> | Callback.       |

```cpp
// Some codecauto onResult = new FDTDGetterStringDelegate();
onResult->BindLambda([](const FString& value)
{
	// Your code...
});
UDTDAnalyticsBPLibrary::GetSdkVersion(*onResult);
```

{% endtab %}

{% tab title="Godot" %}

```gdscript
DTDAnalytics.GetSdkVersion(getSdkVersionHandler)

func getSdkVersionHandler(sdkVersion: String):
    print(sdkVersion)
```

{% endtab %}
{% endtabs %}

### Obtaining user tracking status (GDPR) <a href="#obtaining-user-tracking-status-gdpr" id="obtaining-user-tracking-status-gdpr"></a>

Retrieving the saved state of the user tracking permission by the devtodev system. See “Setting User Tracking Status”. Bool type.

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
DTDAnalytics.getTrackingAvailability { trackingAvailability in
  // your code
}
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

```objectivec
[DTDAnalytics trackingAvailabilityHandler:^(BOOL trackingAvailability) {
  // your code
}];

```

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
DTDAnalytics.getTrackingAvailability { trackingAvailability ->
  // your code
}
```

{% endtab %}

{% tab title="Android (Java)" %}

```java
DTDAnalytics.INSTANCE.getTrackingAvailability( trackingAvailability ->
         // your code
         null
);
```

{% endtab %}

{% tab title=".NET Native + UWP" %}

```csharp
var trackingAvailability = await DTDAnalytics.GetTrackingAvailability();
```

{% endtab %}

{% tab title="Unity" %}

```csharp
DTDAnalytics.GetTrackingAvailability( tracking => {
  //your code
});
```

{% endtab %}

{% tab title="Web" %}

```javascript
const trackingAvailability = analytics.getTrackingAvailability()
```

{% endtab %}

{% tab title="Unreal" %}
![Blueprint](/files/8BfJ0Fgg5FXK0QHG2hha)

| **Argument** | **Type**                                                                             | **Description** |
| ------------ | ------------------------------------------------------------------------------------ | --------------- |
| onResult     | <ul><li>FAnalyticsDynamicGetterBoolDelegate</li><li>FDTDGetterBoolDelegate</li></ul> | Callback.       |

```cpp
auto onResult = new FDTDGetterBoolDelegate();
onResult->BindLambda([](bool value)
{
	// Your code...
});
UDTDAnalyticsBPLibrary::GetTrackingAvailability(*onResult);
```

{% endtab %}

{% tab title="Godot" %}

```gdscript
DTDAnalytics.GetTrackingAvailability(getTrackingAvailabilityHandler)

func getTrackingAvailabilityHandler(trackingAvailability: bool):
    print(str(trackingAvailability))
```

{% endtab %}
{% endtabs %}

### Getting devtodev ID <a href="#getting-devtodev-id" id="getting-devtodev-id"></a>

devtodev ID is the primary numeric identifier for the device/user account in the devtodev database. Using devtodev ID, you are sure to find the user in devtodev.

The identifier will be received from the server some time after the initialization of the SDK.

If you have set counting by users, a separate devtodev id will be issued for each device user.

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}
To obtain the devtodev ID, you need to pass the listener to **`DTDAnalytics`**:

```swift
DTDAnalytics.setIdentifiersListener(listener: self)
```

The delegate must implement the **`func didReceiveDevtodevId(with devtodevId: Int)`**

```swift
func didReceiveDevtodevId(with devtodevId: Int) {
  /// your code
}
```

The **`didReceiveDevtodevId`** method will be called with every ID change on the server side.
{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}
To obtain the devtodev ID, you need to pass the listener to **`DTDAnalytics`**:

```objectivec
@interface Controller ()  <DTDIdentifiersListener>
[DTDAnalytics setIdentifiersListenerWithListener:self];
```

The delegate must implement the **`(void)didReceiveDevtodevIdWith:(NSInteger)devtodevId;`**

```objectivec
- (void)didReceiveDevtodevIdWith:(NSInteger)devtodevId {
  // your code
}
```

The **`didReceiveDevtodevId`** method will be called with every ID change on the server side.
{% endtab %}

{% tab title="Android (Kotlin)" %}
To obtain the devtodev ID, you need to pass the **`DTDIdentifiersListener`** listener to **`DTDAnalytics`**:

```kotlin
DTDAnalytics.setIdentifiersListener(object : DTDIdentifiersListener {
            override fun didReceiveDevtodevId(devtodevId: Long) {
               /// your code
            }
        })
```

The `didReceiveDevtodevId` method will be called with every ID change on the server side.
{% endtab %}

{% tab title="Android (Java)" %}
To obtain the devtodev ID, you need to pass the **`DTDIdentifiersListener`** listener to **`DTDAnalytics`**:

```java
DTDAnalytics.INSTANCE.setIdentifiersListener(new DTDIdentifiersListener() {
            @Override
            public void didReceiveDevtodevId(long devtodevId) {
                // your code
            }
});
```

The `didReceiveDevtodevId` method will be called with every ID change on the server side.
{% endtab %}

{% tab title=".NET Native + UWP" %}
To obtain the devtodev ID, you need to pass the **`DTDIdentifiersListener`** listener delegate to **`DTDAnalytics`**:

```csharp
DTDAnalytics.SetIdentifiersListener(devtodevId =>
{
    // Your code...
});
```

The `delegate` method will be called with every ID change on the server side.
{% endtab %}

{% tab title="Unity" %}
To obtain the devtodev ID, you need to pass the **`DTDIdentifiersListener`** listener delegate to **`DTDAnalytics`**:

```csharp
DTDAnalytics.SetIdentifiersListener(devtodevId =>
{
    // Your code...
});
```

The `delegate` method will be called with every ID change on the server side.
{% endtab %}

{% tab title="Web" %}

```javascript
analytics.setIdentifiersListener((devtodevId) =>
{
// Your code...
})
```

{% endtab %}

{% tab title="Unreal" %}
![Blueprint](/files/gw7DgP6zLE4b427zoCPT)

| **Argument** | **Type**                                                                             | **Description**       |
| ------------ | ------------------------------------------------------------------------------------ | --------------------- |
| listener     | <ul><li>FAnalyticsDynamicGetterLongDelegate</li><li>FDTDGetterLongDelegate</li></ul> | devtodev ID Listener. |

```cpp
auto listener = new FDTDGetterLongDelegate();
listener->BindLambda([](int64 value)
{
	// Your code...
});
UDTDAnalyticsBPLibrary::SetIdentifiersListener(*listener);
```

{% endtab %}

{% tab title="Godot" %}
To obtain the devtodev ID, you need to pass the `Callable` to **`DTDAnalytics`**:

```gdscript
DTDAnalytics.SetIdentifiersCallback(identifiersUpdated)

func identifiersUpdated(devtodevID: int):
    print("SetIdentifiersCallback DevtodevID is " + str(devtodevID))
```

The **`didReceiveDevtodevId`** method will be called with every ID change on the server side.
{% endtab %}
{% endtabs %}

## Initialization callback

To receive a callback when the SDK initialization is complete, you can use a method that will implement the initialization callback. When the SDK completes the initialization, the callback will be called on the main application thread.

{% hint style="info" %}
We recommend implementing a callback before calling initialization.
{% endhint %}

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
DTDAnalytics.setInitializationCompleteCallback {
  print("Initialized has been finished.")
}
let config = DTDAnalyticsConfiguration()
config.logLevel = .error
DTDAnalytics.initialize(applicationKey: "App ID", configuration: config)
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

```objectivec
[DTDAnalytics setInitializationCompleteCallback:^{
  NSLog(@"%@", @"Initialized has been finished.");
}];
DTDAnalyticsConfiguration *config = [[DTDAnalyticsConfiguration alloc] init];
config.logLevel = DTDLogLevelError;
[DTDAnalytics applicationKey:@"App ID" configuration:config];
```

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
DTDAnalytics.setInitializationCompleteCallback {
    Log.d("TAG", "Initialized has been finished.")
}
val config = DTDAnalyticsConfiguration()
config.logLevel = DTDLogLevel.Error
DTDAnalytics.initialize(appKey = "App ID", analyticsConfiguration = config, context = this)
```

{% endtab %}

{% tab title="Android (Java)" %}

```java
DTDAnalytics.INSTANCE.setInitializationCompleteCallback(() -> {
    Log.d("TAG", "Initialized has been finished.");
    return null;
});
DTDAnalyticsConfiguration config = new DTDAnalyticsConfiguration();
config.setLogLevel(DTDLogLevel.Error);
DTDAnalytics.INSTANCE.initialize("App ID", config, this);
```

{% endtab %}

{% tab title=".NET Native + UWP" %}

```csharp
DTDAnalytics.LogLevel = DTDLogLevel.Error;
DTDAnalytics.SetInitializationCompleteCallback(()=>Console.WriteLine($"Initialization has been finished"));
DTDAnalytics.Initialize("App ID");
```

{% endtab %}

{% tab title="Unity" %}

```csharp
DTDAnalytics.SetInitializationCompleteCallback(() =>
{
    Debug.Log("Initialized has been finished.");
});

DTDAnalytics.SetLogLevel(DTDLogLevel.Error);
DTDAnalytics.Initialize("APP_KEY");
```

{% endtab %}

{% tab title="Web" %}

```javascript
analytics.setInitializationCompleteCallback(() =>
{
// Your code...
})
```

{% endtab %}

{% tab title="Godot" %}

```gdscript
func initCompleteCallback():
  print("Initialized has been finished.")

func _ready():
  DTDAnalytics.SetInitializationCompleteCallback(initCompleteCallback)
  var config = GDDTDAnalyticsConfiguration.new()
  config.logLevel = GDDTDLogLevel.Error
  DTDAnalytics.InitializeWithConfig(appKey, config)
```

{% endtab %}
{% endtabs %}

## Fallback proxy

{% hint style="warning" %}
Available for SDK versions:&#x20;

* 2.6.1 (native Android, iOS/macOS) and higher;
* 3.11.0 (Unity) and higher;
* 2.5.0 (.NET Native + UWP) and higher;
* Web 3.0 and higher.
  {% endhint %}

The **`fallbackProxyUrls`** parameter lets you specify one or more proxy server addresses to use when the primary host `https://statgw.devtodev.com` is unreachable from the user's device. The SDK will automatically switch to the next URL in the list if the current host stops responding — for example, if it is blocked by an ISP, a filtering proxy, or an ad-blocking system.

### **When to use**

* Your app's users are located in regions where AWS infrastructure (which hosts devtodev) may be blocked.
* Users' devices have network filters installed (ad-blocking systems, corporate VPNs, DNS blockers) that block the `statgw.devtodev.com` domain.
* You require higher reliability for analytics data delivery.

### **Behavior**

* On initialization, the SDK always starts with the primary URL.
* If a request fails due to a network error (DNS resolution failure, dropped connection, TLS handshake rejection) or a `451` response code, the SDK switches to the next URL in the list.
* All subsequent requests use the new active host.
* If the list is exhausted, the SDK continues using the last URL in the list.
* On the next initialization, the SDK starts again from the primary URL.

### **URL requirements**

* HTTPS protocol is required.
* No trailing slash.
* Each address must point to a proxy server that forwards traffic to `https://statgw.devtodev.com`.
* It is recommended to use domains that do not contain words such as `api`, `ads`, or `track` in their name, as these are more likely to appear in public blocklists.

### **Default value**

An empty list — the fallback mechanism is disabled and only the primary URL is used.

***

The fallback URLs must be set **before** initializing the SDK:

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
DTDAnalytics.fallbackProxyUrls = [
  "https://subdomain1.yourdomain.com",
  "https://subdomain2.yourdomain.com"
]
DTDAnalytics.initialize(applicationKey: "App ID", configuration: config)
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

```objectivec
DTDAnalytics.fallbackProxyUrls = @[
    @"https://subdomain1.yourdomain.com",
    @"https://subdomain2.yourdomain.com"
];
[DTDAnalytics initializeWithApplicationKey:@"App ID" configuration:config];
```

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
DTDAnalytics.fallbackProxyUrls = listOf(
  "https://subdomain1.yourdomain.com",
  "https://subdomain2.yourdomain.com"
)
DTDAnalytics.initialize(applicationKey = "App ID", configuration = config)
```

{% endtab %}

{% tab title="Android (Java)" %}

```java
DTDAnalytics.setFallbackProxyUrls(Arrays.asList(
  "https://subdomain1.yourdomain.com",
  "https://subdomain2.yourdomain.com"
));
DTDAnalytics.initialize("App ID", configuration);
```

{% endtab %}

{% tab title=".NET Native + UWP" %}

```csharp
DTDAnalytics.SetFallbackProxyUrls(new[] {
  "https://subdomain1.yourdomain.com",
  "https://subdomain2.yourdomain.com"
});
```

{% endtab %}

{% tab title="Unity" %}

```csharp
DTDAnalytics.SetFallbackProxyUrls(new[] {
  "https://subdomain1.yourdomain.com",
  "https://subdomain2.yourdomain.com"
});
```

{% endtab %}

{% tab title="Web" %}

```javascript
window.devtodev.setFallbackProxyUrls([
    "https://subdomain1.yourdomain.com",
    "https://subdomain2.yourdomain.com"
]);
window.devtodev.initialize("App ID", initConfig);
```

{% endtab %}
{% endtabs %}

### **Proxy server requirements**

* Forward all traffic to `https://statgw.devtodev.com`.
* Include the client device's IP address in the `X-Real-IP` header when proxying (required for user country detection).


# User profile

## User ID

{% hint style="warning" %}
[Cross-platfrom type projects](/getting-started/adding-an-app-to-the-space/cross-platform-application) use identification by **user ID** by default.&#x20;
{% endhint %}

This method is used to initialize the user in applications where you have set calculation by user ID specified by the developer.

You can also use this method when calculating by device ID (by default) to pass in the user ID used on your servers so that you can easily find the user on devtodev.

In order to track a user on several devices, you can switch the identification method to identification by Custom User Id. This switch can be done only by devtodev – you just need to write a request to the support (use the `Contact Us` form), specifying the space name and project name. You must set the user IDs for all users before changing the identification method.

{% hint style="danger" %}
**Please note: changing the identification method is irreversible and you will not be able to switch back to the device identifiers in the future!**
{% endhint %}

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}
{% hint style="info" %}
We recommend that you pass this parameter in the initialization configuration (**`userId`** property of an instance of th&#x65;**`DTDAnalyticsConfiguration`** class).

Do not pass an empty string ("") to the **`setUserID`** method as the user ID. Assigning an empty string is a command to assign a default value to the user ID (it is equal to the current device ID). In applications with calculation by user ID, this can lead to unnecessary registrations.
{% endhint %}

To set a new value as the user ID, use the **`setUserId`** method.

```swift
DTDAnalytics.setUserId(userId: "Custom User ID")
```

To get the current value of the user ID, use the asynchronous method \
`getDeviceId(_ completionHandler: @escaping (String) -> Void)`

```swift
DTDAnalytics.getUserId { userId in
  // your code
}
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}
{% hint style="info" %}
We recommend that you pass this parameter in the initialization configuration (**`userId`** property of an instance of th&#x65;**`DTDAnalyticsConfiguration`** class).

Do not pass an empty string ("") to the **`setUserID`** method as the user ID. Assigning an empty string is a command to assign a default value to the user ID (it is equal to the current device ID). In applications with calculation by user ID, this can lead to unnecessary registrations.
{% endhint %}

To set a new value as the user ID, use the **`setUserId`** method.

```objectivec
[DTDAnalytics userId:@"Custom User ID"];
```

To get the current value of the user ID, use the asynchronous method \
`(void)deviceIdHandler:( void (^ _Nonnull)(NSString * _Nonnull))completion-Handler;`

```objectivec
[DTDAnalytics userIdHandler:^(NSString * _Nonnull userId) {
  // your code
}];
```

{% endtab %}

{% tab title="Android (Kotlin)" %}
{% hint style="info" %}
We recommend that you pass this parameter in the initialization configuration (**`userId`** property of an instance of th&#x65;**`DTDAnalyticsConfiguration`** class).

Do not pass an empty string ("") to the **`setUserID`** method as the user ID. Assigning an empty string is a command to assign a default value to the user ID (it is equal to the current device ID). In applications with calculation by user ID, this can lead to unnecessary registrations.
{% endhint %}

To set a new value as the user ID, use the **`setUserId`** method.&#x20;

```kotlin
DTDAnalytics.setUserId(userId = "Custom User ID")
```

To get the current value of the user ID, use the asynchronous method \
`getDeviceId(block: (String) -> Unit)`

```kotlin
DTDAnalytics.getUserId { userId ->
  // your code
}
```

{% endtab %}

{% tab title="Android (Java)" %}
{% hint style="info" %}
We recommend that you pass this parameter in the initialization configuration (**`userId`** property of an instance of th&#x65;**`DTDAnalyticsConfiguration`** class).

Do not pass an empty string ("") to the **`setUserID`** method as the user ID. Assigning an empty string is a command to assign a default value to the user ID (it is equal to the current device ID). In applications with calculation by user ID, this can lead to unnecessary registrations.
{% endhint %}

To set a new value as the user ID, use the **`setUserId`** method.&#x20;

```java
DTDAnalytics.INSTANCE.setUserId("Custom User ID");
```

To get the current value of the user ID, use the asynchronous method \
`getDeviceId(block: (String) -> Unit)`

```java
DTDAnalytics.INSTANCE.getUserId(userID ->
      // your code
      null
);
```

{% endtab %}

{% tab title=".NET Native + UWP" %}
{% hint style="info" %}
We recommend that you pass this parameter in the initialization configuration (**`userId`** property of an instance of th&#x65;**`DTDAnalyticsConfiguration`** class).

Do not pass an empty string ("") to the **`SetUserID`** method as the user ID. Assigning an empty string is a command to assign a default value to the user ID (it is equal to the current device ID). In applications with calculation by user ID, this can lead to unnecessary registrations.
{% endhint %}

To set a new value as the user ID, use the **`SetUserId`** method.&#x20;

```csharp
DTDAnalytics.SetUserId(userId: "Custom User ID")
```

To get the current value of the user ID, use the asynchronous method:

```csharp
var userId = await DTDAnalytics.GetUserId();
```

{% endtab %}

{% tab title="Unity" %}
{% hint style="info" %}
We recommend that you pass this parameter in the initialization configuration (**`userId`** property of an instance of th&#x65;**`DTDAnalyticsConfiguration`** class).

Do not pass an empty string ("") to the **`SetUserID`** method as the user ID. Assigning an empty string is a command to assign a default value to the user ID (it is equal to the current device ID). In applications with calculation by user ID, this can lead to unnecessary registrations.
{% endhint %}

To set a new value as the user ID, use the **`SetUserId`** method.&#x20;

```csharp
DTDAnalytics.SetUserId(userId: "Custom User ID")
```

To get the current value of the user ID, use the asynchronous method:

```csharp
DTDAnalytics.GetUserId(id =>
{
  //your code
});
```

{% endtab %}

{% tab title="Web" %}
{% hint style="info" %}
We recommend that you pass this parameter in the initialization configuration (**`userId`** property in config object).

Do not pass an empty string ("") to the **`setUserID`** method as the user ID. Assigning an empty string is a command to assign a default value to the user ID (it is equal to the current device ID). In applications with calculation by user ID, this can lead to unnecessary registrations.
{% endhint %}

To set a new value as the user ID, use the **`setUserId`** method.

```javascript
analytics.setUserId("Custom User ID")
```

To get the current value of the user ID, use the **`getUserId`**&#x6D;ethod&#x20;

```javascript
const userId = analytics.getUserId()
```

{% endtab %}

{% tab title="Unreal" %}
{% hint style="info" %}
We recommend that you pass this parameter in the initialization configuration (***`UserId`*** property of an instance of the **`FDTDAnalyticsConfiguration`** class).

Do not pass an empty string ("") to the **`SetUserID`** method as the user ID. Assigning an empty string is a command to assign a default value to the user ID (it is equal to the current device ID). In applications with calculation by user ID, this can lead to unnecessary registrations.
{% endhint %}

To set a new value as the user ID, use method:

![Blueprint](/files/zGleHi7gLvgGE7aI4TzG)

| Arguments      | Type    | Description |
| -------------- | ------- | ----------- |
| ***`userId`*** | FString | User ID.    |

```csharp
UDTDAnalyticsBPLibrary::SetUserId("Name");
```

To get the current value of the user ID, use the asynchronous method:

![Blueprint](/files/FGJkwlU0IpKlKD9YC0K2)

| Arguments        | Type                                                                                     | Description |
| ---------------- | ---------------------------------------------------------------------------------------- | ----------- |
| ***`onResult`*** | <ul><li>FAnalyticsDynamicGetterStringDelegate</li><li>FDTDGetterStringDelegate</li></ul> | Callback    |

```csharp
auto onResult = new FDTDGetterStringDelegate();
onResult->BindLambda([](const FString& value)
{
	// Your code...
});
UDTDAnalyticsBPLibrary::GetUserId(*onResult);
```

{% endtab %}

{% tab title="Godot" %}
{% hint style="info" %}
We recommend that you pass this parameter in the initialization configuration (**`userId`** property of an instance of the **GDDTDAnalyticsConfiguration** class).

Do not pass an empty string ("") to the **`setUserID`** method as the user ID. Assigning an empty string is a command to assign a default value to the user ID (it is equal to the current device ID). In applications with calculation by user ID, this can lead to unnecessary registrations.
{% endhint %}

To set a new value as the user ID, use the **`SetUserId`** method.

```gdscript
DTDAnalytics.SetUserId("Custom User ID")
```

To get the current value of the user ID, use the asynchronous method \
`DTDAnalytics.GetUserId(`onResult: `Callable)`

```gdscript
DTDAnalytics.GetUserId(getUserIdHandler)

func getUserIdHandler(userId: String): 
  print(userId)
```

{% endtab %}
{% endtabs %}

## Replace

This method is used in very rare cases. It is applicable in the case when the calculation of users in the project is carried out by the user ID specified by the developer. In this case, the application can change this identifier. For example, calculation in the application is carried out by user emails and in the application, it is possible to change this email to another one. At the time of replacement, you need to call this method, specifying the user's previous email and the one with which it was replaced.

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
DTDAnalytics.replace(fromUserId: "Old user id", toUserId: "New user id")
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

```objectivec
[DTDAnalytics replaceFromUserId:@"Old user id" toUserId:@"New user id"];
```

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
DTDAnalytics.replaceUserId(
    fromUserId = "Old user id", 
    toUserId = "New user id"
)
```

{% endtab %}

{% tab title="Android (Java)" %}

```java
DTDAnalytics.INSTANCE.replaceUserId("Old user id", "New user id");
```

{% endtab %}

{% tab title=".NET Native + UWP" %}

```csharp
DTDAnalytics.Replace(fromUserId: "Old user id", toUserId: "New user id");
```

{% endtab %}

{% tab title="Unity" %}

```csharp
DTDAnalytics.Replace(fromUserId: "Old user id", toUserId: "New user id");
```

{% endtab %}

{% tab title="Web" %}

```javascript
analytics.replaceUserId( "Old user id", "New user id")
```

{% endtab %}

{% tab title="Unreal" %}
![Blueprint](/files/4OjwMjKEdvvkecqZCRnw)

| Arguments          | Type    | Description  |
| ------------------ | ------- | ------------ |
| ***`fromUserId`*** | FString | From user ID |
| ***`toUserId`***   | FString | To user ID   |

```cpp
UDTDAnalyticsBPLibrary::ReplaceUserId("UserIdBefore", "UserIdAfter");
```

{% endtab %}

{% tab title="Godot" %}

```gdscript
DTDAnalytics.ReplaceUserId("Old user id", "New user id")
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Do not use this method to re-login as a different user! The setUserId method is used for relogging.
{% endhint %}

## Current user level

This method is used in cross-platform and data-synchronised applications.\
The method is required to update user-level data.

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}
To set the current value to the user level, use **`setCurrentLevel(currentLevel: Int)`** method:

```swift
DTDAnalytics.setCurrentLevel(value: 2)
```

We recommend that you use the **`setCurrentLevel`** method immediately after using the [**`setUserID`**](/integration/integration-of-sdk-v2/setting-up-events/user-profile#user-id) method or specify the current user level in the initialization configuration (the level property of an instance of the **`DTDAnalyticsConfiguration`**).

{% hint style="warning" %}
Do not use the **`setCurrentLevel`** method when the user reaches a new level. In this case, you must use the [**`levelUp`**](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#level-up) method.
{% endhint %}

To get the user-level value stored by the SDK, use **`getCurrentLevel(completionHandler: @escaping (Int) -> Void)`** method

```swift
DTDAnalytics.getCurrentLevel { level in
  // your code
}
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}
To set the current value to the user level, use **`(void)currentLevel:(NSInteger)currentLevel;`** method:

```objectivec
[DTDAnalytics currentLevel:2];
```

We recommend that you use the **`setCurrentLevel`** method immediately after using the [**`setUserID`**](#user-id) method or specify the current user level in the initialization configuration (the level property of an instance of the **`DTDAnalyticsConfiguration`**).

{% hint style="warning" %}
Do not use the **`setCurrentLevel`** method when the user reaches a new level. In this case, you must use the [**`levelUp`**](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#level-up) method.
{% endhint %}

To get the user-level value stored by the SDK, use **`(void)currentLevelHandler:( void (^ _Nonnull)(NSInteger))completionHandler;`** method

```objectivec
[DTDAnalytics currentLevelHandler:^(NSInteger level) {
  // your code
}];
```

{% endtab %}

{% tab title="Android (Kotlin)" %}
To set the current value to the user level, use method:

```kotlin
DTDAnalytics.setCurrentLevel(currentLevel = 2)
```

We recommend that you use the **`setCurrentLevel`** method immediately after using the [**`setUserID`**](#user-id) method or specify the current user level in the initialization configuration (the level property of an instance of the **`DTDAnalyticsConfiguration`**).

{% hint style="warning" %}
Do not use the **`setCurrentLevel`** method when the user reaches a new level. In this case, you must use the [**`levelUp`**](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#level-up) method.
{% endhint %}

To get the user-level value stored by the SDK, use **`getCurrentLevel(completionHandler: @escaping (Int) -> Void)`** method

```kotlin
DTDAnalytics.getCurrentLevel { level ->
  // your code
}
```

{% endtab %}

{% tab title="Android (Java)" %}
To set the current value to the user level, use method:

```swift
DTDAnalytics.INSTANCE.setCurrentLevel(2);
```

We recommend that you use the **`setCurrentLevel`** method immediately after using the [**`setUserID`**](#user-id) method or specify the current user level in the initialization configuration (the level property of an instance of the **`DTDAnalyticsConfiguration`**).

{% hint style="warning" %}
Do not use the **`setCurrentLevel`** method when the user reaches a new level. In this case, you must use the [**`levelUp`**](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#level-up) method.
{% endhint %}

To get the user-level value stored by the SDK, use **`getCurrentLevel(completionHandler: @escaping (Int) -> Void)`** method

```java
DTDAnalytics.INSTANCE.getCurrentLevel ( currentLevel ->
       // your code 
       null
);
```

{% endtab %}

{% tab title=".NET Native + UWP" %}
To set the current value to the user level, use method:

```csharp
DTDAnalytics.SetCurrentLevel(level: 3);
```

We recommend that you use the **`SetCurrentLevel`** method immediately after using the [**`SetUserID`**](#user-id) method or specify the current user level in the initialization configuration (the level property of an instance of the **`DTDAnalyticsConfiguration`**).

{% hint style="warning" %}
Do not use the **`SetCurrentLevel`** method when the user reaches a new level. In this case, you must use the [**`LevelUp`**](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#level-up) method.
{% endhint %}

To get the user-level value stored by the SDK, use **`GetCurrentLevel`** method:

```csharp
var currentLevel = await DTDAnalytics.GetCurrentLevel();
```

{% endtab %}

{% tab title="Unity" %}
To set the current value to the user level, use method:

```csharp
DTDAnalytics.SetCurrentLevel(level: 3);
```

We recommend that you use the **`SetCurrentLevel`** method immediately after using the [**`SetUserID`**](#user-id) method or specify the current user level in the initialization configuration (the level property of an instance of the **`DTDAnalyticsConfiguration`**).

{% hint style="warning" %}
Do not use the **`SetCurrentLevel`** method when the user reaches a new level. In this case, you must use the [**`LevelUp`**](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#level-up) method.
{% endhint %}

To get the user-level value stored by the SDK, use **`GetCurrentLevel`** method:

```csharp
DTDAnalytics.GetCurrentLevel(level =>
{
  //your code
});
```

{% endtab %}

{% tab title="Web" %}
To set the current value to the user level, use method:

```javascript
analytics.setCurrentLevel(2)
```

We recommend that you use the **`setCurrentLevel`** method immediately after using the [**`setUserID`**](#user-id) method or specify the current user level in the initialization configuration (the level property of an instance of the **`DTDAnalyticsConfiguration`**).

{% hint style="warning" %}
Do not use the **`setCurrentLevel`** method when the user reaches a new level. In this case, you must use the [**`levelUp`**](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#level-up) method.
{% endhint %}

To get the user-level value stored by the SDK, use **`getCurrentLevel`** method.

```javascript
const currentLevel = analytics.getCurrentLevel()
```

{% endtab %}

{% tab title="Unreal" %}
To set the current value to the user level, use method:

![Blueprint](/files/Yby8nm9klCWtwX7EPnsc)

| Arguments     | Type  | Description    |
| ------------- | ----- | -------------- |
| ***`level`*** | int32 | Current level. |

```cpp
UDTDAnalyticsBPLibrary::SetCurrentLevel(7);
```

We recommend that you use the **`SetCurrentLevel`** method immediately after using the [**`SetUserID`**](#user-id) method or specify the current user level in the initialization configuration (the level property of an instance of the **`EDTDAnalyticsConfiguration`**).

{% hint style="warning" %}
Do not use the **`SetCurrentLevel`** method when the user reaches a new level. In this case, you must use the [**`LevelUp`**](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#level-up) method.
{% endhint %}

To get the user-level value stored by the SDK, use method:

![Blueprint](/files/ZV4JR4vBw052rgc2KXGq)

| Arguments        | Type                                                                               | Description |
| ---------------- | ---------------------------------------------------------------------------------- | ----------- |
| ***`onResult`*** | <ul><li>FAnalyticsDynamicGetterIntDelegate</li><li>FDTDGetterIntDelegate</li></ul> | Callback    |

```cpp
auto onResult = new FDTDGetterIntDelegate();
onResult->BindLambda([](int32 value)
{
	// Your code...
});
UDTDAnalyticsBPLibrary::GetCurrentLevel(*onResult);
```

{% endtab %}

{% tab title="Godot" %}
To set the current value to the user level, use **`SetCurrentLevel(level: int)`** method:

```gdscript
DTDAnalytics.SetCurrentLevel(2)
```

We recommend that you use the **`SetCurrentLevel`** method immediately after using the [**`SetUserID`**](#user-id) method or specify the current user level in the initialization configuration (the level property of an instance of the **`GDDTDAnalyticsConfiguration`**).

{% hint style="warning" %}
Do not use the **`SetCurrentLevel`** method when the user reaches a new level. In this case, you must use the [**`LevelUp`**](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#level-up) method.
{% endhint %}

To get the user-level value stored by the SDK, use **`GetCurrentLevel(onResult: Callable)`** method

```gdscript
DTDAnalytics.GetCurrentLevel(getCurrentLevelHandler)

func getCurrentLevelHandler(level: int):
    print("result is: " + str(level))
```

{% endtab %}
{% endtabs %}

## Cheater

If you have your own methods for detecting cheaters in the application, you can tag such users. Actions taken by these users will not be counted in statistics.&#x20;

{% hint style="warning" %}
If you need to exclude transactions from such users from statistics, go to **Users & Segments** section in devtodev interface and [mark the user](/reports-and-functionality/project-related-reports-and-fuctionality/users#mark-as-cheater-or-tester) manually. \
When you mark the user in devtodev interface, the system removes their transactions for the last 7 days the reports and recalculates the metrics.&#x20;
{% endhint %}

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
DTDUserCard.setCheater(cheater: true)
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

```objectivec
[DTDUserCard setCheater:true];
```

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
DTDUserCard.setCheater(cheater = true)
```

{% endtab %}

{% tab title="Android (Java)" %}

```java
DTDUserCard.INSTANCE.setCheater(true);
```

{% endtab %}

{% tab title=".NET Native + UWP" %}

```csharp
DTDUserCard.SetCheater(cheater: true);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
DTDUserCard.SetCheater(cheater: true);
```

{% endtab %}

{% tab title="Web" %}

```javascript
analytics.user.setCheater(true)
```

{% endtab %}

{% tab title="Unreal" %}

<figure><img src="/files/Hy5AiTydC33Bvh2dlsyt" alt=""><figcaption><p>Blueprint</p></figcaption></figure>

| Arguments       | Type | Description  |
| --------------- | ---- | ------------ |
| ***`cheater`*** | bool | Cheater flag |

```cpp
UDTDUserCardBPLibrary::SetCheater(true);
```

{% endtab %}

{% tab title="Godot" %}

```gdscript
DTDUserCard.SetCheater(true)
```

{% endtab %}
{% endtabs %}

## Tester

Use this method to tag a user as a tester. Events performed by testers will not be included in statistics.&#x20;

{% hint style="warning" %}
If you need to exclude transactions from such users from statistics, go to **Users & Segments** section in devtodev interface and [mark the user](/reports-and-functionality/project-related-reports-and-fuctionality/users#mark-as-cheater-or-tester) manually. \
When you mark the user in devtodev interface, the system removes their transactions for the last 7 days the reports and recalculates the metrics.&#x20;
{% endhint %}

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
DTDUserCard.setTester(tester: true)
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

<pre class="language-objectivec"><code class="lang-objectivec"><strong>[DTDUserCard setTester:true];
</strong></code></pre>

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
DTDUserCard.setTester(tester = true)
```

{% endtab %}

{% tab title="Android (Java)" %}

```java
DTDUserCard.INSTANCE.setTester(true);
```

{% endtab %}

{% tab title=".NET Native + UWP" %}

```csharp
DTDUserCard.SetTester(tester: true);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
DTDUserCard.SetTester(tester: true);
```

{% endtab %}

{% tab title="Web" %}

```javascript
analytics.user.setTester(true)
```

{% endtab %}

{% tab title="Unreal" %}
![Blueprint](/files/pwycuueuCBDleDBoWELL)

| Arguments      | Type | Description |
| -------------- | ---- | ----------- |
| ***`tester`*** | bool | Tester flag |

<pre class="language-cpp"><code class="lang-cpp"><strong>UDTDUserCardBPLibrary::SetTester(true);
</strong></code></pre>

{% endtab %}

{% tab title="Godot" %}

```gdscript
DTDUserCard.SetTester(true)
```

{% endtab %}
{% endtabs %}

## Reserved user properties

{% hint style="danger" %}
**Attention! These properties have been removed since the devtodev SDK versions:**\
**iOS & macOS 2.4.0, Android 2.5.0, Unity SDK 3.8.0, Godot 1.0.0, Web 2.1**

**We strongly recommend not to use these properties because they refer to** [**personal data**](https://gdpr-info.eu/issues/personal-data/)**.**&#x20;
{% endhint %}

{% tabs fullWidth="true" %}
{% tab title="iOS + macOS" %}
{% hint style="warning" %}
Attention! We strongly discourage the storage of personal user data! If you plan to pass this data, be sure to indicate this in the ‘Nutrition label’ when submitting the application to the App Store review.
{% endhint %}

| Property     | Getter                             | Setter                                                            |
| ------------ | ---------------------------------- | ----------------------------------------------------------------- |
| Name         | **`setName(name: String)`**        | **`getName(completionHandler: @escaping (String?) -> Void)`**     |
| Email        | **`setEmail(email: String)`**      | **`getEmail(completionHandler: @escaping (String?) -> Void)`**    |
| Phone        | **`setPhone(phone: String)`**      | **`getPhone(completionHandler: @escaping (String?) -> Void)`**    |
| Photo        | **`setPhoto(photo: String)`**      | **`getPhoto(completionHandler: @escaping (String?) -> Void)`**    |
| Gender       | **`setGender(gender: DTDGender)`** | **`getGender(completionHandler: @escaping (DTDGender) -> Void)`** |
| Age          | **`setAge(age: Int)`**             | **`getAge(completionHandler: @escaping (Int) -> Void`**           |
| {% endtab %} |                                    |                                                                   |

{% tab title="iOS+macOS (Objective-C)" %}
{% hint style="warning" %}
Attention! We strongly discourage the storage of personal user data! If you plan to pass this data, be sure to indicate this in the ‘Nutrition label’ when submitting the application to the App Store review.
{% endhint %}

| Property     | Setter                                              | Getter                                                          |
| ------------ | --------------------------------------------------- | --------------------------------------------------------------- |
| Age          | **`[DTDUserCard setAge:(NSInteger)];`**             | **`[DTDUserCard getAgeHandler:^(NSInteger age) { }]`**          |
| Email        | **`[DTDUserCard setEmail:(NSString * _Nonnull)];`** | **`[DTDUserCard getEmailHandler:^(NSString * email) { }];`**    |
| Gender       | **`[DTDUserCard setGender:(enum Gender)];`**        | **`[DTDUserCard getGenderHandler:^(enum Gender gender) { }];`** |
| Name         | **`[DTDUserCard setName:(NSString * _Nonnull)];`**  | **`[DTDUserCard getNameHandler:^(NSString * name) { }];`**      |
| Phone        | **`[DTDUserCard setPhone:(NSString * _Nonnull)];`** | **`[DTDUserCard getPhoneHandler:^(NSString * phone) { }];`**    |
| Photo        | **`[DTDUserCard setPhoto:(NSString * _Nonnull)];`** | **`[DTDUserCard getPhotoHandler:^(NSString * photo) { }];`**    |
| {% endtab %} |                                                     |                                                                 |

{% tab title="Android (Kotlin)" %}

| Property     | Getter                             | Setter                                        |
| ------------ | ---------------------------------- | --------------------------------------------- |
| Name         | **`setName(name: String)`**        | **`getName(handler: (String) -> Unit)`**      |
| Email        | **`setEmail(email: String)`**      | **`getEmail(handler: (String) -> Unit)`**     |
| Phone        | **`setPhone(phone: String)`**      | **`getPhone(handler: (String) -> Unit)`**     |
| Photo        | **`setPhoto(photo: String)`**      | **`getPhoto(handler: (String) -> Unit)`**     |
| Gender       | **`setGender(gender: DTDGender)`** | **`getGender(handler: (DTDGender) -> Void)`** |
| Age          | **`setAge(age: Int)`**             | **`getAge(handler: (String) -> Unit)`**       |
| {% endtab %} |                                    |                                               |

{% tab title="Android (Java)" %}

| Property     | Getter                             | Setter                                        |
| ------------ | ---------------------------------- | --------------------------------------------- |
| Name         | **`setName(name: String)`**        | **`getName(handler: (String) -> Unit)`**      |
| Email        | **`setEmail(email: String)`**      | **`getEmail(handler: (String) -> Unit)`**     |
| Phone        | **`setPhone(phone: String)`**      | **`getPhone(handler: (String) -> Unit)`**     |
| Photo        | **`setPhoto(photo: String)`**      | **`getPhoto(handler: (String) -> Unit)`**     |
| Gender       | **`setGender(gender: DTDGender)`** | **`getGender(handler: (DTDGender) -> Void)`** |
| Age          | **`setAge(age: Int)`**             | **`getAge(handler: (String) -> Unit)`**       |
| {% endtab %} |                                    |                                               |

{% tab title=".NET Native + UWP" %}

| Property     | Getter                             | Setter                            |
| ------------ | ---------------------------------- | --------------------------------- |
| Name         | **`SetName(name: string)`**        | **`Task<string> GetName();`**     |
| Email        | **`SetEmail(email: string)`**      | **`Task<string> GetEmail()`**     |
| Phone        | **`SetPhone(phone: string)`**      | **`Task<string> GetPhone()`**     |
| Photo        | **`SetPhoto(photo: string)`**      | **`Task<string> GetPhoto()`**     |
| Gender       | **`SetGender(gender: DTDGender)`** | **`Task<DTDGender> GetGender()`** |
| Age          | **`SetAge(age: long)`**            | **`Task<long> GetAge()`**         |
| {% endtab %} |                                    |                                   |

{% tab title="Unity" %}
{% hint style="warning" %}
Attention! We strongly discourage the storage of personal user data! If you plan to pass this data, be sure to indicate this in the ‘Nutrition label’ when submitting the application to the App Store review.
{% endhint %}

| Property     | Getter                             | Setter                                         |
| ------------ | ---------------------------------- | ---------------------------------------------- |
| Name         | **`SetName(name: string)`**        | **`GetName(Action<string> onGetName)`**        |
| Email        | **`SetEmail(email: string)`**      | **`GetEmail(Action<string> onGetEmail)`**      |
| Phone        | **`SetPhone(phone: string)`**      | **`GetPhone(Action<string> onGetPhone)`**      |
| Photo        | **`SetPhoto(photo: string)`**      | **`GetPhoto(Action<string> onGetPhoto)`**      |
| Gender       | **`SetGender(gender: DTDGender)`** | **`GetGender(Action<DTDGender> onGetGender)`** |
| Age          | **`SetAge(age: long)`**            | **`GetAge(Action<string> onGetAge)`**          |
| {% endtab %} |                                    |                                                |

{% tab title="Web" %}

| Property     | Getter            | Setter                                         | Type   |
| ------------ | ----------------- | ---------------------------------------------- | ------ |
| Name         | **`getName()`**   | **`setName("John Doe")`**                      | String |
| Email        | **`getEmail()`**  | **`setEmail("email@me.com")`**                 | String |
| Phone        | **`getPhone()`**  | **`setPhone("+15555555555")`**                 | String |
| Photo        | **`getPhoto()`**  | **`setPhoto("https://domain.com/photo.jpg")`** | String |
| Gender       | **`getGender()`** | **`setGender(gender)`**                        |        |
| Age          | **`getAge()`**    | **`setAge(age)`**                              | Int    |
| {% endtab %} |                   |                                                |        |

{% tab title="Unreal" %}
{% hint style="warning" %}
Attention! We strongly discourage the storage of personal user data! If you plan to pass this data, be sure to indicate this in the ‘Nutrition label’ when submitting the application to the App Store review.
{% endhint %}

| Property       | Setter                              | Getter                                                                                                                                                                                        |
| -------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`Name`***   | **`SetName(name: FString)`**        | <ul><li><strong><code>GetName(delegate: FUserCardDynamicGetterStringDelegate)</code></strong></li><li><strong><code>GetName(delegate: FDTDGetterStringDelegate)</code></strong></li></ul>     |
| ***`Email`***  | **`SetEmail(email: FString)`**      | <ul><li><strong><code>GetEmail(delegate: FUserCardDynamicGetterStringDelegate)</code></strong></li><li><strong><code>GetEmail(delegate: FDTDGetterStringDelegate)</code></strong></li></ul>   |
| ***`Phone`***  | **`SetPhone(phone: FString)`**      | <ul><li><strong><code>GetPhone(delegate: FUserCardDynamicGetterStringDelegate)</code></strong></li><li><strong><code>GetPhone(delegate: FDTDGetterStringDelegate)</code></strong></li></ul>   |
| ***`Photo`***  | **`SetPhoto(photo: FString)`**      | <ul><li><strong><code>GetPhoto(delegate: FUserCardDynamicGetterStringDelegate)</code></strong></li><li><strong><code>GetPhoto(delegate: FDTDGetterStringDelegate)</code></strong></li></ul>   |
| ***`Gender`*** | **`SetGender(gender: EDTDGender)`** | <ul><li><strong><code>GetGender(delegate: FUserCardDynamicGetterGenderDelegate)</code></strong></li><li><strong><code>GetGender(delegate: FDTDGetterGenderDelegate)</code></strong></li></ul> |
| ***`Age`***    | **`SetAge(age: int64)`**            | <ul><li><strong><code>GetAge(delegate: FUserCardDynamicGetterLongDelegate)</code></strong></li><li><strong><code>GetAge(delegate: FDTDGetterLongDelegate)</code></strong></li></ul>           |

Example:

![Blueprint](/files/dlZHrHyqR5VvgIe1bBgc)

```cpp
UDTDUserCardBPLibrary::SetEmail("Email");
```

![Blueprint](/files/jduAAokLFYqgxbn9m93x)

```cpp
auto onResult = new FDTDGetterStringDelegate();
onResult->BindLambda([](const FString& value)
{
	// Your code...
});
UDTDUserCardBPLibrary::GetEmail(*onResult);
```

{% endtab %}
{% endtabs %}

## Custom user property

Each devtodev project can have **up to 30 custom user properties**. User custom property values can be a number, a string (up to 500 symbols), or a boolean value.

{% hint style="warning" %}
Attention! We strongly recommend that you do not use these properties to transfer and store data that fits the definition of [personal data](https://gdpr-info.eu/issues/personal-data/)!
{% endhint %}

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}
This is how you can set properties on the current user profile:

```swift
DTDUserCard.set(key: "key for string value", value: "string value")
DTDUserCard.set(key: "key for int value", value: 10)
DTDUserCard.set(key: "key for double value", value: 12.5)
DTDUserCard.set(key: "key for bool value", value: true)
```

{% hint style="info" %}
It is important to remember that the key for custom user properties has a length limit. If the limit is exceeded (from 1 to 64 characters), the key will be truncated to the maximum allowed length
{% endhint %}

To get the current value stored in the user profile on the SDK, you need to use the method:

**`getValue(key: String, _completionHandler: @escaping (Any) -> Void)`**

```swift
DTDUserCard.getValue(key: "key for value") { value in
  // your code
}
```

{% hint style="info" %}
When using the **`getValue`** method, note that the **`Any`** return type will need to be cast to the data type you want.
{% endhint %}
{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}
This is how you can set properties on the current user profile:

```objectivec
[DTDUserCard setString:@"key for string value" value:@"string value"];
[DTDUserCard setInt:@"key for int value" value:10];
[DTDUserCard setDouble:@"key for double value" value:12.5];
[DTDUserCard setBool:@"key for bool value" value:true];
```

{% hint style="info" %}
It is important to remember that the key for custom user properties has a length limit. If the limit is exceeded (from 1 to 64 characters), the key will be truncated to the maximum allowed length
{% endhint %}

To get the current value stored in the user profile on the SDK, you need to use the method:

**`(void)getValueWithKey:(NSString *  _Nonnull)key :(void (^ _Nonnull)(id _Nullable))completionHandler;`**

```objectivec
[DTDUserCard getValueWithKey:@"key for value" :^(id object) {
  // your code
}];
```

{% hint style="info" %}
When using the **`getValue`** method, note that the **`Any`**&#x72;eturn type will need to be cast to the data type you want.
{% endhint %}
{% endtab %}

{% tab title="Android (Kotlin)" %}
This is how you can set properties on the current user profile:

```kotlin
DTDUserCard.set(key = "key for string value", value = "string value")
DTDUserCard.set(key = "key for int value", value = 10)
DTDUserCard.set(key = "key for double value", value = 12.5)
DTDUserCard.set(key = "key for bool value", value = true)
```

{% hint style="info" %}
It is important to remember that the key for custom user properties has a length limit. If the limit is exceeded (from 1 to 64 characters), the key will be truncated to the maximum allowed length
{% endhint %}

To get the current value stored in the user profile on the SDK, you need to use the method:

**`getValue(key: String, handler: (Any?) -> Unit)`**

```kotlin
DTDUserCard.getValue(key = "key for value") { value ->
  // your code
}
```

{% hint style="info" %}
When using the **`getValue`** method, note that the **`Any`**&#x72;eturn type will need to be cast to the data type you want.
{% endhint %}
{% endtab %}

{% tab title="Android (Java)" %}
This is how you can set properties on the current user profile:

```java
DTDUserCard.INSTANCE.set("key for string value", "string value");
DTDUserCard.INSTANCE.set("key for int value", 10);
DTDUserCard.INSTANCE.set("key for double value", 12.5);
DTDUserCard.INSTANCE.set("key for bool value", true);
```

{% hint style="info" %}
It is important to remember that the key for custom user properties has a length limit. If the limit is exceeded (from 1 to 64 characters), the key will be truncated to the maximum allowed length
{% endhint %}

To get the current value stored in the user profile on the SDK, you need to use the method:

**`getValue(key: String, handler: (Any?) -> Unit)`**

```java
DTDUserCard.INSTANCE.getValue("key for value", value ->
       // your code
       null
);j
```

{% hint style="info" %}
When using the **`getValue`** method, note that the **`Any`**&#x72;eturn type will need to be cast to the data type you want.
{% endhint %}
{% endtab %}

{% tab title=".NET Native + UWP" %}
This is how you can set properties on the current user profile:

```csharp
DTDUserCard.Set(key: "key for string value", value: "string value");
DTDUserCard.Set(key: "key for int value", value: 10);
DTDUserCard.Set(key: "key for double value", value: 12.5);
DTDUserCard.Set(key: "key for bool value", value: true);
```

{% hint style="info" %}
It is important to remember that the key for custom user properties has a length limit. If the limit is exceeded (from 1 to 64 characters), the key will be truncated to the maximum allowed length
{% endhint %}

To get the current value stored in the user profile on the SDK, you need to use the method:

```csharp
var value = await DTDUserCard.Get(key: "key for value");
switch (value)
{
    case bool boolValue:
        break;
    case long longValue:
        break;
    case double doubleValue:
        break;
    case string stringValue:
        break;
}
```

{% hint style="info" %}
When using the **`Get`** method, note that the **`object`** return type will need to be cast to the data type you want.
{% endhint %}
{% endtab %}

{% tab title="Unity" %}
This is how you can set properties on the current user profile:

```csharp
DTDUserCard.Set(key: "key for string value", value: "string value");
DTDUserCard.Set(key: "key for int value", value: 10);
DTDUserCard.Set(key: "key for double value", value: 12.5);
DTDUserCard.Set(key: "key for bool value", value: true);
```

{% hint style="info" %}
It is important to remember that the key for custom user properties has a length limit. If the limit is exceeded (from 1 to 64 characters), the key will be truncated to the maximum allowed length
{% endhint %}

To get the current value stored in the user profile on the SDK, you need to use the method:

```csharp
DTDUserCard.GetValue("key", value =>
{
    switch (value)
    {
    case bool boolValue:
        break;
    case long longValue:
        break;
    case double doubleValue:
        break;
    case string stringValue:
        break;
    }             
})
DTDUserCard.Get(key: "key for value");
```

{% hint style="info" %}
When using the **`Get`** method, note that the **`object`** return type will need to be cast to the data type you want.
{% endhint %}
{% endtab %}

{% tab title="Web" %}
This is how you can set properties on the current user profile:

```javascript
analytics.user.set("key for string value",  "string value")
analytics.user.set("key for int value", 10)
analytics.user.set("key for double value", 12.5)
analytics.user.set("key for bool value", true)
```

{% hint style="info" %}
It is important to remember that the key for custom user properties has a length limit. If the limit is exceeded (from 1 to 64 characters), the key will be truncated to the maximum allowed length
{% endhint %}

To get the current value stored in the user profile on the SDK, you need to use the method:

```javascript
analytics.user.getValue("key for value") 
```

{% endtab %}

{% tab title="Unreal" %}
This is how you can set properties on the current user profile:

![Blueprint](/files/mQQYp6DkCEOy94ha9yJi)

| Arguments   | Type    | Description      |
| ----------- | ------- | ---------------- |
| ***key***   | FString | Parameter key.   |
| ***value*** | bool    | Parameter value. |

```cpp
UDTDUserCardBPLibrary::SetBool("BoolKey", true);
```

![Blueprint](/files/5P0g47nPtivvBFReeslj)

| Arguments   | Type    | Description      |
| ----------- | ------- | ---------------- |
| ***key***   | FString | Parameter key.   |
| ***value*** | float   | Parameter value. |

```cpp
UUDTDUserCardBPLibrary::SetFloat("FloatKey", 3.333);
```

![Blueprint](/files/8qjvqJZBBsfPb2acFC00)

| Arguments   | Type    | Description      |
| ----------- | ------- | ---------------- |
| ***key***   | FString | Parameter key.   |
| ***value*** | int64   | Parameter value. |

```cpp
UDTDUserCardBPLibrary::SetLong("LongKey", 1000);
```

![Blueprint](/files/IFV1sBJvT2JonmWKa86O)

| Arguments   | Type    | Description      |
| ----------- | ------- | ---------------- |
| ***key***   | FString | Parameter key.   |
| ***value*** | FString | Parameter value. |

```cpp
UDTDUserCardBPLibrary::SetString("StringKey", "StringValue");
```

{% hint style="info" %}
It is important to remember that the key for custom user properties has a length limit. If the limit is exceeded (from 1 to 64 characters), the key will be truncated to the maximum allowed length
{% endhint %}

To get the current value stored in the user profile on the SDK, you need to use methods:

![Blueprint](/files/VRHpHEIgsSi12NvcF27Y)

| Arguments      | Type                                                                                                       | Description    |
| -------------- | ---------------------------------------------------------------------------------------------------------- | -------------- |
| ***key***      | FString                                                                                                    | Parameter key. |
| ***onResult*** | <ul><li>FUserCardDynamicGetterOptionalBoolDelegate</li><li>FDTDGetterOptionalBoolWithKeyDelegate</li></ul> | Callback.      |

```cpp
FString key = FString(TEXT("Key"));
auto onResult = new FDTDGetterOptionalBoolWithKeyDelegate();
onResult->BindLambda([](bool success, const FString& key, bool value)
{
	// Your code...
});
UDTDUserCardBPLibrary::TryGetBool(key, *onResult);
```

![Blueprint](/files/MFDqEETbiWndDpV4BUJj)

| Arguments      | Type                                                                                                         | Description    |
| -------------- | ------------------------------------------------------------------------------------------------------------ | -------------- |
| ***key***      | FString                                                                                                      | Parameter key. |
| ***onResult*** | <ul><li>FUserCardDynamicGetterOptionalFloatDelegate</li><li>FDTDGetterOptionalFloatWithKeyDelegate</li></ul> | Callback.      |

```cpp
FString key = FString(TEXT("Key"));
auto onResult = new FDTDGetterOptionalFloatWithKeyDelegate();
onResult->BindLambda([](bool success, const FString& key, float value)
{
	// Your code...
});
UDTDUserCardBPLibrary::TryGetFloat(key, *onResult);
```

![Blueprint](/files/QunRj88bL4KNhQUBA7oA)

| Arguments      | Type                                                                                                       | Description    |
| -------------- | ---------------------------------------------------------------------------------------------------------- | -------------- |
| ***key***      | FString                                                                                                    | Parameter key. |
| ***onResult*** | <ul><li>FUserCardDynamicGetterOptionalLongDelegate</li><li>FDTDGetterOptionalLongWithKeyDelegate</li></ul> | Callback.      |

```cpp
FString key = FString(TEXT("Key"));
auto onResult = new FDTDGetterOptionalIntWithKeyDelegate();
onResult->BindLambda([](bool success, const FString& key, int64 value)
{
	// Your code...
});
UDTDUserCardBPLibrary::TryGetLong(key, *onResult);
```

&#x20;

![Blueprint](/files/eeYoAHxG0rEoXk6KRlBZ)

| Arguments      | Type                                                                                                           | Description    |
| -------------- | -------------------------------------------------------------------------------------------------------------- | -------------- |
| ***key***      | FString                                                                                                        | Parameter key. |
| ***onResult*** | <ul><li>FUserCardDynamicGetterOptionalStringDelegate</li><li>FDTDGetterOptionalStringWithKeyDelegate</li></ul> | Callback.      |

```cpp
FString key = FString(TEXT("Key"));
auto onResult = new FDTDGetterOptionalStringWithKeyDelegate();
onResult->BindLambda([](bool success, const FString& key, const FString& value)
{
	// Your code...
});
UDTDUserCardBPLibrary::TryGetString(key, *onResult);
```

{% endtab %}

{% tab title="Godot" %}
This is how you can set properties on the current user profile:

```gdscript
DTDUserCard.SetString("key for string value", "string value")
DTDUserCard.SetInt("key for int value", 10)
DTDUserCard.SetFloat("key for double value", 12.5)
DTDUserCard.SetBool("key for bool value", true)
```

{% hint style="info" %}
It is important to remember that the key for custom user properties has a length limit. If the limit is exceeded (from 1 to 64 characters), the key will be truncated to the maximum allowed length
{% endhint %}

To get the current values stored in the user profile on the SDK, you need to use the methods:

**`TryGetBool(key: String, callback: Callable)`**

**`TryGetFloat(key: String, callback: Callable)`**

**`TryGetInt(key: String, callback: Callable)`**

**`TryGetString(key: String, callback: Callable`**

```gdscript
DTDUserCard.TryGetInt("intKey", getIntHandler)
DTDUserCard.TryGetString("intKey", getStringHandler)
DTDUserCard.TryGetFloat("intKey", getFloatHandler)
DTDUserCard.TryGetBool("intKey", getBoolHandler)

func getIntHandler(isValid: bool, value: int):
	print("isValid = " + str(isValid) + " integer = " + str(value))

func getStringHandler(isValid: bool, value: String):
	print("isValid = " + str(isValid) + " string = " + value)
	
func getFloatHandler(isValid: bool, value: float):
	print("isValid = " + str(isValid) + " float = " + str(value))
	
func getBoolHandler(isValid: bool, value: bool):
	print("isValid = " + str(isValid) + " bool = " + str(value))
```

{% hint style="info" %}
The **isValid** parameter is responsible for the presence of the requested key in the user card.
{% endhint %}
{% endtab %}
{% endtabs %}

## Unset user property

It removes a property or a list of properties and their values from the current user profile.&#x20;

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
DTDUserCard.unset(property: "key for string value")
DTDUserCard.unset(properties: ["key for string value", 
                               "key for int value"])
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

```objectivec
[DTDUserCard unsetProperty:@"key for string value"];
[DTDUserCard unset:@[@"key for string value",
                     @"key for int value"]];
```

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
DTDUserCard.unset(property = "key for string value")
DTDUserCard.unset(property = listOf("key for string value", "key for int value"))
```

{% endtab %}

{% tab title="Android (Java)" %}

```java
DTDUserCard.INSTANCE.unset( "key for string value");

ArrayList<String> properties = new ArrayList<>();
properties.add("key for string value");
properties.add("key for int value");
DTDUserCard.INSTANCE.unset(properties);
```

{% endtab %}

{% tab title=".NET Native + UWP" %}

```csharp
DTDUserCard.Unset(keys: "key 1");
DTDUserCard.Unset(keys: "key 1", "key 2", "key 3");
```

{% endtab %}

{% tab title="Unity" %}

```csharp
DTDUserCard.Unset(keys: "key 1");
DTDUserCard.Unset(keys: "key 1", "key 2", "key 3");
```

{% endtab %}

{% tab title="Web" %}

```javascript
analytics.user.unset("key for string value")
analytics.user.unset(["key for string value", "key for int value"])
```

{% endtab %}

{% tab title="Unreal" %}
![Blueprint](/files/BqZSUixPwQvTmSrHPBim)

| Arguments | Type    | Description    |
| --------- | ------- | -------------- |
| ***key*** | FString | Parameter key. |

```cpp
UDTDUserCardBPLibrary::Unset("Key");
```

![Blueprint](/files/YynmquBHCcZIKVR3xqy0)

| Arguments  | Type             | Description     |
| ---------- | ---------------- | --------------- |
| ***keys*** | TArray\<FString> | Parameter keys. |

```cpp
TArray<FString> keys;
keys.Add("Key1");
keys.Add("Key2");
UDTDUserCardBPLibrary::UnsetArray(keys);
```

{% endtab %}

{% tab title="Godot" %}

```gdscript
DTDUserCard.Unset("intKey")

var arrayOfKeys = ["floatKey", "boolKey"]
DTDUserCard.UnsetArray(arrayOfKeys)
```

{% endtab %}
{% endtabs %}

## Unset all user properties

To remove all properties from the user card, use:

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
DTDUserCard.clearUser()
```

{% hint style="info" %}
Keep in mind that the ***cheater*** mark is not cleared from the user card; you can only uncheck the mark manually using the [**`setCheater`**](#cheater) method.
{% endhint %}
{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

```objectivec
[DTDUserCard clearUser];
```

{% hint style="info" %}
Keep in mind that the ***cheater*** mark is not cleared from the user card; you can only uncheck the mark manually using the [**`setCheater`**](#cheater) method.
{% endhint %}
{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
DTDUserCard.clearUser()
```

{% hint style="info" %}
Keep in mind that the ***cheater*** mark is not cleared from the user card; you can only uncheck the mark manually using the [**`setCheater`**](#cheater) method.
{% endhint %}
{% endtab %}

{% tab title="Android (Java)" %}

```java
DTDUserCard.INSTANCE.clearUser();
```

{% hint style="info" %}
Keep in mind that the ***cheater*** mark is not cleared from the user card; you can only uncheck the mark manually using the [**`setCheater`**](#cheater) method.
{% endhint %}
{% endtab %}

{% tab title=".NET Native + UWP" %}

```csharp
DTDUserCard.ClearUser();
```

{% hint style="info" %}
Keep in mind that the ***cheater*** mark is not cleared from the user card; you can only uncheck the mark manually using the [**`SetCheater`**](#cheater) method.
{% endhint %}
{% endtab %}

{% tab title="Unity" %}

```csharp
DTDUserCard.ClearUser();
```

{% hint style="info" %}
Keep in mind that the ***cheater*** mark is not cleared from the user card; you can only uncheck the mark manually using the [**`SetCheater`**](#cheater) method.
{% endhint %}
{% endtab %}

{% tab title="Web" %}

```javascript
analytics.user.clearUser()
```

{% hint style="info" %}
Keep in mind that the ***cheater*** mark is not cleared from the user card; you can only uncheck the mark manually using the [**`setCheater`**](#cheater) method.
{% endhint %}
{% endtab %}

{% tab title="Unreal" %}
![Blueprint](/files/bFhmYvjGapzEVMuA1ryx)

```cpp
UDTDUserCardBPLibrary::ClearUser();
```

{% hint style="info" %}
Keep in mind that the ***cheater*** mark is not cleared from the user card; you can only uncheck the mark manually using the [**`SetCheater`**](#cheater) method.
{% endhint %}
{% endtab %}

{% tab title="Godot" %}

```gdscript
DTDUserCard.ClearUser()
```

{% hint style="info" %}
Keep in mind that the ***cheater*** mark is not cleared from the user card; you can only uncheck the mark manually using the [**`setCheater`**](#cheater) method.
{% endhint %}
{% endtab %}
{% endtabs %}


# Anticheat methods

## Recommended sequence of actions when working with transactions

1. Get response about a completed transaction from the payment system.
2. Either send data about the received transaction for verification by calling devtodev anti-cheat methods or use your own tools for transaction verification.
3. If the transaction has successfully passed verification, perform the Payment event. \
   If the transaction has not passed verification, do not perform the Payment event.&#x20;

## Payment Validation

The devtodev service allows you to validate transactions to prevent fraud from influencing your statistics. For this, you need to integrate **`DTDAntiCheat`** module.

{% hint style="danger" %}
We strongly discourage you from using verification results for deciding on allowing or denying users to receive their purchases! We do not recommend to mark users as cheaters based on the results of this verification! **Employ this method exclusively for preventing fraud transaction data from being sent to devtodev!**
{% endhint %}

{% tabs fullWidth="true" %}
{% tab title="App Store (iOS) Swift" %}
To validate the transaction you can use the **`verifyPayment(completionHandler: @escaping (DTDVerifyResponse) -> Void)`** method immediately during the transaction processing, e.g.:

```swift
extension Purchases: SKPaymentTransactionObserver {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
                case .purchased:
                    DTDAntiCheat.verifyPayment { response in
                            switch response.receiptStatus {
                                case .receiptInternalError: 
                                  // your code
                                  break
                                case .receiptValid:
                                  // your code
                                  break
                                case .receiptSandbox:
                                  // your code
                                  break
                                case .receiptServerError:
                                  // your code
                                  break
                                case .receiptNotValid: 
                                  // your code
                                  break
                                @unknown default: break
                            }
                            SKPaymentQueue.default().finishTransaction(transaction)
                        }

                case .restored:
                    SKPaymentQueue.default().finishTransaction(transaction)

                case .failed:
                    SKPaymentQueue.default().finishTransaction(transaction)

                default:
                    break
            }
        }
    }
}

```

## `DTDVerifyResponse`

The DTDVerifyResponse object returned while validating the transaction has two properties:

| Property                 | Description                                                                                |
| ------------------------ | ------------------------------------------------------------------------------------------ |
| **`receiptStatus`**      | Enum type **`DTDReceiptStatus`** that represents the result of the transaction validation. |
| **`verificationResult`** | Additional information from the validation server.                                         |

## `DTDReceiptStatus`

The enum type returned as a result of validation can receive the following values:

| **Value**                    | Description                                                          |
| ---------------------------- | -------------------------------------------------------------------- |
| ***`receiptValid`***         | The payment is valid, the transaction is genuine.                    |
| ***`receiptNotValid`***      | The payment is invalid, the transaction may be a duplicate or fraud. |
| ***`receiptServerError`***   | Server error when validating the payment.                            |
| ***`receiptSandbox`***       | Test payment.                                                        |
| ***`receiptInternalError`*** | Internal SDK error.                                                  |

{% hint style="info" %}
We recommend calling the Real Currency Payment method in all cases except when you receive ***`receiptNotValid`*** or ***`receiptSandbox`*** as a result of the validation.
{% endhint %}
{% endtab %}

{% tab title="App Store (iOS) Objective-C" %}
To validate the transaction you can use the **`(void)verifyPaymentCompletion:( void (^ _Nonnull)(DTDVerifyResponse * _Nonnull))completionHandler;`** method immediately during the transaction processing, e.g.:

```objectivec
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{
    for(SKPaymentTransaction *transaction in transactions) {
        switch(transaction.transactionState){
            case SKPaymentTransactionStatePurchasing: {
                // Your code ...
                break;
            }

            case SKPaymentTransactionStatePurchased: {
                // Your code ...
                [DTDAntiCheat verifyPaymentCompletion:^(DTDVerifyResponse * _Nonnull response) {
                    switch ([response receiptStatus]) {
                        case ReceiptStatusReceiptInternalError: {

                            break;
                        }
                        case ReceiptStatusReceiptValid: {

                            break;
                        }
                        case ReceiptStatusReceiptSandbox: {

                            break;
                        }
                        case ReceiptStatusReceiptServerError: {

                            break;
                        }
                        case ReceiptStatusReceiptNotValid: {

                            break;
                        }
                    }
                }];
                break;
            }

            case SKPaymentTransactionStateRestored: {
                // Your code ...
                break;
            }

            case SKPaymentTransactionStateFailed: {
                // Your code ...
                break;
            }

            case SKPaymentTransactionStateDeferred: {
                // Your code ...
                break;
            }
        }
    }
}
```

## `DTDVerifyResponse`

The DTDVerifyResponse object returned while validating the transaction has two properties:

| Property                 | Description                                                                                |
| ------------------------ | ------------------------------------------------------------------------------------------ |
| **`receiptStatus`**      | Enum type **`DTDReceiptStatus`** that represents the result of the transaction validation. |
| **`verificationResult`** | Additional information from the validation server.                                         |

## `DTDReceiptStatus`

The enum type returned as a result of validation can receive the following values:

<table data-header-hidden><thead><tr><th width="373">Value</th><th>Description</th></tr></thead><tbody><tr><td><strong>Value</strong></td><td>Description</td></tr><tr><td><em><strong><code>receiptValid</code></strong></em></td><td>The payment is valid, the transaction is genuine.</td></tr><tr><td><em><strong><code>receiptNotValid</code></strong></em></td><td>The payment is invalid, the transaction may be a duplicate or fraud.</td></tr><tr><td><em><strong><code>receiptServerError</code></strong></em></td><td>Server error when validating the payment.</td></tr><tr><td><em><strong><code>receiptSandbox</code></strong></em></td><td>Test payment.</td></tr><tr><td><em><strong><code>receiptInternalError</code></strong></em></td><td>Internal SDK error.</td></tr></tbody></table>

{% hint style="info" %}
We recommend calling the Real Currency Payment method in all cases except when you receive ***`receiptNotValid`*** or ***`receiptSandbox`*** as a result of the validation.
{% endhint %}
{% endtab %}

{% tab title="Google Play (Kotlin)" %}
When Google Play sends the transaction back to your **`onActivityResult`**, validate it by calling the following method: **`verifyPayment(receipt: String, signature: String, publicKey: String, completionHandler:(DTDVerifyResponse) -> Unit)`** immediately during the transaction processing, e.g.:

```kotlin
DTDAntiCheat.verifyPayment(
    receipt = "receipt", 
    signature = "signature", 
    publickKey = "publickKey"
) { dtdVerifyResponse ->
    val verificationResult = dtdVerifyResponse.verificationResult
    /* your code here */
    when (dtdVerifyResponse.receiptStatus) {
        DTDReceiptStatus.ReceiptValid -> { /* your code here */ }
        DTDReceiptStatus.ReceiptNotValid -> { /* your code here */ }
        DTDReceiptStatus.ReceiptServerError -> { /* your code here */ }
        DTDReceiptStatus.ReceiptInternalError -> { /* your code here */ }
    }
}
```

## `DTDVerifyResponse`

The DTDVerifyResponse object returned while validating the transaction has two properties:

| Property                 | Description                                                                                |
| ------------------------ | ------------------------------------------------------------------------------------------ |
| **`receiptStatus`**      | Enum type **`DTDReceiptStatus`** that represents the result of the transaction validation. |
| **`verificationResult`** | Additional information from the validation server.                                         |

## `DTDReceiptStatus`

The enum type returned as a result of validation can receive the following values:

| **Value**                    | Description                                                          |
| ---------------------------- | -------------------------------------------------------------------- |
| ***`receiptValid`***         | The payment is valid, the transaction is genuine.                    |
| ***`receiptNotValid`***      | The payment is invalid, the transaction may be a duplicate or fraud. |
| ***`receiptServerError`***   | Server error when validating the payment.                            |
| ***`receiptInternalError`*** | Internal SDK error.                                                  |

{% hint style="info" %}
We recommend calling the Real Currency Payment method in all cases except when you receive ***`receiptNotValid`*** as a result of the validation.
{% endhint %}
{% endtab %}

{% tab title="Google Play (Java)" %}
When Google Play sends the transaction back to your **`onActivityResult`**, validate it by calling the following method: **`verifyPayment(receipt: String, signature: String, publicKey: String, completionHandler:(DTDVerifyResponse) -> Unit)`** immediately during the transaction processing, e.g.:

```java
DTDAntiCheat.INSTANCE.verifyPayment("receipt", "signature", "publickKey",
                dtdVerifyResponse -> {
                // your code
                    switch (dtdVerifyResponse.getReceiptStatus()) {
                        case ReceiptValid:
                            // your code
                            break;
                        case ReceiptNotValid:
                            // your code
                            break;
                        case ReceiptInternalError:
                            // your code
                            break;
                        case ReceiptServerError:
                            // your code
                            break;
                    }
                    return null;
                });
```

## `DTDVerifyResponse`

The DTDVerifyResponse object returned while validating the transaction has two properties:

| Property                 | Description                                                                                |
| ------------------------ | ------------------------------------------------------------------------------------------ |
| **`receiptStatus`**      | Enum type **`DTDReceiptStatus`** that represents the result of the transaction validation. |
| **`verificationResult`** | Additional information from the validation server.                                         |

## `DTDReceiptStatus`

The enum type returned as a result of validation can receive the following values:

| **Value**                    | Description                                                          |
| ---------------------------- | -------------------------------------------------------------------- |
| ***`receiptValid`***         | The payment is valid, the transaction is genuine.                    |
| ***`receiptNotValid`***      | The payment is invalid, the transaction may be a duplicate or fraud. |
| ***`receiptServerError`***   | Server error when validating the payment.                            |
| ***`receiptInternalError`*** | Internal SDK error.                                                  |

{% hint style="info" %}
We recommend calling the Real Currency Payment method in all cases except when you receive ***`receiptNotValid`*** as a result of the validation.
{% endhint %}
{% endtab %}

{% tab title="Microsoft Store (UWP)" %}
devtodev sends a request for transaction verification to the payment platform and then forwards the answer to the app.\
To validate the transaction you can use the **`Task<DTDReceiptStatus> VerifyPayment(string: receipt)`** method. As an argument pass the **`PurchaseResults.ReceiptXml`** property. More information about it [here](https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.store.purchaseresults.receiptxml?view=winrt-20348#Windows_ApplicationModel_Store_PurchaseResults_ReceiptXml).

Example of verification:

```csharp
var result = await DTDAntiCheat.VerifyPayment("receipt");
switch (result)
{
    case DTDReceiptStatus.Valid:
        break;
    case DTDReceiptStatus.Invalid:
        break;
    case DTDReceiptStatus.ServerError:
        break;
    case DTDReceiptStatus.InternalError:
        break;
    default:
        throw new ArgumentOutOfRangeException();
}
```

## `DTDVerifyResponse`

The DTDVerifyResponse object returned while validating the transaction has two properties:

| Property                 | Description                                                                                |
| ------------------------ | ------------------------------------------------------------------------------------------ |
| **`ReceiptStatus`**      | Enum type **`DTDReceiptStatus`** that represents the result of the transaction validation. |
| **`VerificationResult`** | Additional information from the validation server.                                         |

## `DTDReceiptStatus`

The enum type returned as a result of validation can receive the following values:

| Value                      | Description                                                         |
| -------------------------- | ------------------------------------------------------------------- |
| ***`Valid = 0L`***         | The payment is valid, the transaction went through successfully     |
| ***`Invalid = 1L`***       | The payment is invalid, the transaction may be a duplicate or fraud |
| ***`ServerError = 2L`***   | Server error when validating the payment                            |
| ***`InternalError = 4L`*** | Internal SDK error                                                  |

{% hint style="info" %}
We recommend calling the Real Currency Payment method in all cases except when you receive ***`Invalid`*** as a result of the validation.
{% endhint %}
{% endtab %}

{% tab title="Unity (3 stores )" %}

## Google Play

If you use Unity IAP for payment validation, call the following method: **`void VerifyPayment(string publicKey, string receipt, Action completionHandler)`**

Example:

```csharp
public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs e)
{
    DTDAntiCheat.VerifyPayment(yourPublicKey, e.purchasedProduct.receipt, result =>
    {
        if (result.ReceiptStatus == DTDReceiptVerificationStatus.ReceiptValid ||
            result.ReceiptStatus == DTDReceiptVerificationStatus.ReceiptInternalError || 
            result.ReceiptStatus == DTDReceiptVerificationStatus.ReceiptServerError)
        {
            // Code for valid result.
        }
        else
        {
            // Code for invalid result.
        }
    });
}
```

To validate data received from Google Play, use **`void VerifyPayment(string publicKey, string receipt, string signature,Action<DTDVerifyResponse> completionHandler)`** when handling the transaction.

```csharp
public void MyNativeCallback (string publicKey, string receipt, string signature)
{
    DTDAntiCheat.VerifyPayment(publicKey, receipt, signature, result =>
    {
        if (result.ReceiptStatus == DTDReceiptVerificationStatus.ReceiptValid ||
            result.ReceiptStatus == DTDReceiptVerificationStatus.ReceiptInternalError || 
            result.ReceiptStatus == DTDReceiptVerificationStatus.ReceiptServerError)
        {
            // Code for valid result.
        }
        else
        {
            // Code for invalid result.
        }
    });
}
```

Here's how to find your app's public key for licensing (for Google Play platform only, for other platforms the publicKey is not used):

1. Go to the Google Play Console and sign in. Make sure that you sign in to the account from which the app you are licensing is published (or will be published).
2. In the app details page, locate the Services & APIs link and click it.
3. In the Services & APIs page, locate the Licensing & In-App Billing section. Your public key for licensing is given in the Your License Key For This Application field.

## App Store

If you use Unity IAP for payment validation, call the following method: **`void VerifyPayment(string receipt, Action completionHandler)`**

```csharp
public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs e)
{
    DTDAntiCheat.VerifyPayment(e.purchasedProduct.receipt, result =>
    {
        if (result.ReceiptStatus == DTDReceiptVerificationStatus.ReceiptValid ||
            result.ReceiptStatus == DTDReceiptVerificationStatus.ReceiptInternalError || 
            result.ReceiptStatus == DTDReceiptVerificationStatus.ReceiptServerError)
        {
            // Code for valid result.
        }
        else
        {
            // Code for invalid result.
        }
    });
}
```

## Windows Store (UWP)

If you use Unity IAP for payment validation, call the following method: **`void VerifyPayment(string receipt, Action completionHandler)`**

```csharp
public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs e)
{
    DTDAntiCheat.VerifyPayment(e.purchasedProduct.receipt, result =>
    {
        if (result.ReceiptStatus == DTDReceiptVerificationStatus.ReceiptValid ||
            result.ReceiptStatus == DTDReceiptVerificationStatus.ReceiptInternalError || 
            result.ReceiptStatus == DTDReceiptVerificationStatus.ReceiptServerError)
        {
            // Code for valid result.
        }
        else
        {
            // Code for invalid result.
        }
    });
}
```

{% hint style="info" %}
N.B. You can pass a native XML recipe to the **receipt** argument.
{% endhint %}

## `DTDVerifyResponse`

The DTDVerifyResponse object returned while validating the transaction has two properties:

| Property                 | Description                                                                                |
| ------------------------ | ------------------------------------------------------------------------------------------ |
| **`receiptStatus`**      | Enum type **`DTDReceiptStatus`** that represents the result of the transaction validation. |
| **`verificationResult`** | Additional information from the validation server.                                         |

## `DTDReceiptStatus`

The enum type returned as a result of validation can receive the following values:

<table data-header-hidden><thead><tr><th width="373">Value</th><th>Description</th></tr></thead><tbody><tr><td><strong>Value</strong></td><td>Description</td></tr><tr><td><em><strong><code>receiptValid</code></strong></em></td><td>The payment is valid, the transaction is genuine.</td></tr><tr><td><em><strong><code>receiptNotValid</code></strong></em></td><td>The payment is invalid, the transaction may be a duplicate or fraud.</td></tr><tr><td><em><strong><code>receiptServerError</code></strong></em></td><td>Server error when validating the payment.</td></tr><tr><td><em><strong><code>receiptSandbox</code></strong></em></td><td>Test payment.</td></tr><tr><td><em><strong><code>receiptInternalError</code></strong></em></td><td>Internal SDK error.</td></tr></tbody></table>

{% hint style="info" %}
We recommend calling the Real Currency Payment method in all cases except when you receive ***`receiptNotValid`*** or ***`receiptSandbox`*** as a result of the validation.
{% endhint %}
{% endtab %}
{% endtabs %}


# Track sessions

Session Measurement and Tracking in Mobile and Web Applications

## How devtodev SDK tracks sessions

Session measurement is an important metric for product analysis as it allows us to determine how frequently and for how long users interact with our website or application. However, it is important to note that session tracking methods on mobile and web applications have their own peculiarities.&#x20;

When a user starts a session in the application, the SDK recognizes that the application is active, indicating that it has gained focus (when the app is brought into the foreground). If the last recorded activity was more than 10 minutes ago, a **Session Start** event is sent.

**Application activity** refers to the period of time when the application is in focus, meaning the application or web page is open and the device screen is active. The focus is lost if the application goes into the background or if another website is opened in the current tab.

We measure the duration of application activity using a technical event called **User Engagement (UE)**. It starts counting the time as soon as the application receives focus and sends the activity counter data to the server.&#x20;

If, for any reason, the information about the duration of the activity could not be sent, it will be sent the next time the application is initialized and has internet access. However, the activity will only be included in devtodev reports if it has been less than 7 days since the session, as events from a previous period more than 7 days ago are ignored.&#x20;

Thus, we have information about "Session start" and the duration of activity, but there is no specific "Session end" event. &#x20;

All events performed by the user are marked with the session start date on which they occurred (`sessionid` field in SQL tables).

## Platform specifics&#x20;

### Mobile applications

It is difficult to determine the beginning and end of a session because users often switch between screens of different applications. If an application on a mobile device receives focus and the last active time (in focus) was more than 10 minutes ago, a new session will start and a **Session Start** event will be sent to devtodev.&#x20;

For example, the user opens the application, spends a minute in it, and then puts the application in the background, a **Session Start** event will be sent to devtodev in the first second. After a minute, when the application goes into the background or is closed, an event with information about the duration of activity (**UE**) will be sent to the server as the focus is lost.

### Web projects

It is not possible to detect when the user closes the page. Therefore, the **UE** event (duration of activity) is sent to the server every 2 minutes. To minimize the loss of information about the session duration to no more than two minutes in case of session termination, the SDK additionally saves the duration every 5 seconds and will send the information about the last duration upon the next activity. If there is no next session, the information about the last two minutes may be lost.&#x20;

Let's consider an example where a user opens a webpage, spends 1.5 minutes on it, then opens another page on the site and spends another 1.5 minutes there.&#x20;

A **Session Start** event will be sent in the first second, and every 5 seconds, information about the activity will be saved. After 2 minutes from the start of the session, a **UE** event with 2 minutes of activity will be sent to the server, and after the third minute, the activity of 1 minute will be recorded in the Local Storage. Information about this activity will be sent during the next user session.&#x20;

### Windows

The SDK cannot control app activity for Windows Standalone projects hence this responsibility is passed on to the developer. During the SDK initialization, the activity is triggered automatically, and later the activity status will not change automatically.&#x20;

For tracking app activity, the developer can use the **`DTDAnalytics.StartActivity`** and **`DTDAnalytics.StopActivity`** methods.&#x20;

It is recommended that you use the **`DTDAnalytics.StopActivity`** method to stop the activity when the app goes into the background or being closed. If the window is re-opened from the taskbar it is recommended to renew the activity by using the **`DTDAnalytics.StartActivity`** method.&#x20;

{% content-ref url="/pages/-MkwFB-F75tzaUO1ocsq" %}
[Windows](/integration/integration-of-sdk-v2/sdk-integration/windows)
{% endcontent-ref %}

{% content-ref url="/pages/pM2V35giI3H9bAMJXJ7P" %}
[Unity](/integration/integration-of-sdk-v2/sdk-integration/unity)
{% endcontent-ref %}

## Session metrics in reports

In [Basic Metrics](/reports-and-functionality/project-related-reports-and-fuctionality/events-and-funnels#basic-metrics), Engagement -> [Sessions](/reports-and-functionality/project-related-reports-and-fuctionality/smart-view/engagement-reports#sessions), and [other reports](/basic-events-and-custom-events#reports-based-on-a-session-event), we encounter the following metrics:

* **Session duration** – average session time of one user. Calculated as (Total Sessions Length / Number of sessions) averaged by users.&#x20;
* **Number of sessions** – average number of sessions per user. Calculated as the Number of sessions divided by the Number of users.&#x20;
* **Total daily time spent** – average total time per day spent in the user application. Calculated as Total Sessions Length divided by the number of Active Users.&#x20;
* **Sessions -** total number of sessions (opening or unfolding the application) for the given time period.&#x20;
* **Sessions by user** – average number of sessions made by one user during the period.&#x20;
* **Average session length** – calculated from the data obtained from session starts and user activity time during those sessions. It is defined as the sum of the length of all sessions divided by the number of sessions within a given period.&#x20;

## Session metrics in SQL

### SQL Wizard

In the SQL wizard, there is a parameter called `session.Duration`, which is tracked by the **UE** event. The `session.Duration` parameter represents the duration of the activity, i.e., the time the application is in focus, and it is not equal to the session duration.

`sessions.Count` is the number of **Session Start** events received from the user.

### SQL Editor

The [`sessions`](/reports-and-functionality/space-related-reports-and-functionality/sql#sessions-table-specific-fields) table has two types of `eventtype` field in SQL:&#x20;

* **ss**: represents the **Session Start** event received from the user.
* **ue**: represents **User Engagement** – the time that the application was in focus (active), providing information about time parameters and activity duration.&#x20;

From this data, you can calculate the average session length by dividing the sum of activity lengths from all rows for the desired period by the sum of all session starts for the same period. We recommend using extended time periods to obtain a more reliable result.


# Push notifications

## Available platforms

<table data-view="cards" data-full-width="false"><thead><tr><th></th><th data-hidden data-card-cover data-type="image">Cover image</th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td>Android</td><td><a href="/files/x5jaSMjh2hJ5qSyPXL4e">/files/x5jaSMjh2hJ5qSyPXL4e</a></td><td><a href="/pages/-MhEsj6fAJzoaFu_A54c">/pages/-MhEsj6fAJzoaFu_A54c</a></td></tr><tr><td>iOS</td><td><a href="/files/xrQLgVzNaa2zLJDjtvD2">/files/xrQLgVzNaa2zLJDjtvD2</a></td><td><a href="/pages/-MhDthIzlL5VqbWJJ3ds">/pages/-MhDthIzlL5VqbWJJ3ds</a></td></tr><tr><td>Windows</td><td><a href="/files/GgBXjppNkdffL51GHtvM">/files/GgBXjppNkdffL51GHtvM</a></td><td><a href="/pages/-MkwJ4kCdieWGytiGXOM">/pages/-MkwJ4kCdieWGytiGXOM</a></td></tr><tr><td>Unity</td><td><a href="/files/myIguBP6LYSo0AcMknha">/files/myIguBP6LYSo0AcMknha</a></td><td><a href="/pages/mbIcIe9GBs8KLJNTSL2E">/pages/mbIcIe9GBs8KLJNTSL2E</a></td></tr><tr><td>Unreal Engine</td><td><a href="/files/7q7PQ5lJ4w88F6iwNiWi">/files/7q7PQ5lJ4w88F6iwNiWi</a></td><td><a href="/pages/vGe9jEueqoDXU2tR47MM">/pages/vGe9jEueqoDXU2tR47MM</a></td></tr></tbody></table>

## Set up push campaigns in devtodev&#x20;

{% content-ref url="/pages/-M-Q4AWHh2YwhaWlZt3o" %}
[Push Notifications](/reports-and-functionality/project-related-reports-and-fuctionality/experiments/push-notifications)
{% endcontent-ref %}


# Android

Android Push Notifications

## How to create a project in Firebase and integrate Firebase Services into your application

Push Notifications on Android are sent with the help of the FCM service.

Register and open the project creation window in Firebase.

<figure><img src="/files/BoMfecWVKyEXjbjaS5Yd" alt="" width="563"><figcaption></figcaption></figure>

Click Add project.

<figure><img src="/files/trVmPQ4yudkDOckc3RJ5" alt="" width="563"><figcaption></figcaption></figure>

Write the name of your project. At this stage, you can also set your own unique project identifier or use the one that Firebase will generate for you automatically.

<figure><img src="/files/4zWpq3foZ1dIKrE2q0fh" alt="" width="563"><figcaption></figcaption></figure>

After creating the project card, create an Android application by clicking on the Android icon.

<figure><img src="/files/DHlEqS16zW4mr3IfRQSr" alt="" width="375"><figcaption></figcaption></figure>

Register your android package name.

<figure><img src="/files/BWJWaHN0TvTV3lAdx7u0" alt="" width="563"><figcaption></figcaption></figure>

Download the google-services.json file and use it according to the instructions of firebase.

<figure><img src="/files/u8rAnOop3hvsN6o819WI" alt="" width="563"><figcaption></figcaption></figure>

Add firebase dependencies according to the firebase documentation.

<figure><img src="/files/CefyA4QcZQEZgmnptmFD" alt="" width="563"><figcaption></figcaption></figure>

Complete the application registration, you will see the project overview section.

<figure><img src="/files/mhrOQJbww8fH0pFcm1D8" alt="" width="563"><figcaption></figcaption></figure>

Select your application by clicking on it, you will see a gear on the right side, click on it to go to project settings.

<figure><img src="/files/63pRJFjNUPea4usnatw8" alt="" width="533"><figcaption></figcaption></figure>

Go to the cloud messages section, make sure that the Firebase Cloud Messaging API (V1) is active (if not, activate it).

<figure><img src="/files/wPPODWigaLr0iiAlkAJr" alt="" width="563"><figcaption></figcaption></figure>

Go to the general section and copy the value of the Project ID field and paste it into the devtodev web interface.

<figure><img src="/files/YGKg6q2cc7OkClWcix15" alt="" width="563"><figcaption></figcaption></figure>

The Project ID is also available from the Firebase main screen in the project cards. After setting up the application, the card should look like this:

<figure><img src="/files/ZT382kpiLSSTp7moHBF7" alt="" width="563"><figcaption></figcaption></figure>

It should have a Project ID and an android icon.

Next, open the push notifications integration settings panel in the application settings section in devtodev service (App → Settings → Push notifications → Push notifications panel). Push edit button (pencil symbol).

<figure><img src="/files/GIFXIrjC3C991Y4JhH4L" alt=""><figcaption></figcaption></figure>

You will need to specify the Firebase Project ID and authorize devtodev to send messages and manage messaging subscriptions for your Firebase application. To authorise the application, you must use Google login and password of a user with sufficient access rights to the project on Firebase.\
After that click Save button.

## Messaging Module Integration

The Messaging module is available as an AAR (recommended) and JAR library. The library is available in the MavenCentral and [GitHub repository](https://github.com/devtodev-analytics/android-sdk-2.0).

1\. If you use Gradle for the applications build, add **`mavenCentral()`** into ***gradle.build*** file of your application and specify the following relationship in dependencies block:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
defaultConfig {
    //other existing fields 
    versionName = "1.0" //your app version (required)
}

dependencies {
    // Requirement
    implementation("androidx.appcompat:appcompat:*.*.*")
    implementation("com.google.code.gson:gson:*.*.*")
    implementation("com.google.firebase:firebase-messaging:*.*.*")
    implementation("com.google.android.gms:play-services-ads-identifier:*.*.*")
    
    // if you use AAR (recommended) or JAR downloaded from GitHub, please add:
    implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
    
    // or just add the dependency, get the latest version from
    // https://mvnrepository.com/artifact/com.devtodev/android-analytics
    implementation("com.devtodev:android-analytics:*.*.*")
    // https://mvnrepository.com/artifact/com.devtodev/android-messaging
    implementation("com.devtodev:android-messaging:*.*.*")
    
    // Optional (recommended)
    implementation("com.android.installreferrer:installreferrer:*.*")
}
```

{% endtab %}

{% tab title="Groovy" %}

```groovy
defaultConfig {
    //other existing fields 
    versionName "1.0" //your app version (required)
}

dependencies {
    // Requirement
    implementation 'androidx.appcompat:appcompat:*.*.*'
    implementation 'com.google.code.gson:gson:*.*.*'
    implementation 'com.google.android.gms:play-services-ads-identifier:*.*.*'
    implementation 'com.google.firebase:firebase-messaging:*.*.*'
    
    // if you use AAR (recommended) or JAR downloaded from GitHub, please add:
    implementation fileTree(dir: "libs", include: ["*.aar"]) 
    
    // or just add the dependency, get the latest version from
    // https://mvnrepository.com/artifact/com.devtodev/android-analytics
    implementation 'com.devtodev:android-analytics:*.*.*'
    // https://mvnrepository.com/artifact/com.devtodev/android-messaging
    implementation 'com.devtodev:android-messaging:*.*.*'
    
    // Optional (recommended)
    implementation 'com.android.installreferrer:installreferrer:*.*'
}
```

{% endtab %}
{% endtabs %}

2\. To the app manifest add the following:

<pre class="language-xml"><code class="lang-xml">&#x3C;!-- permission.POST_NOTIFICATIONS for API level 33 or higher -->
&#x3C;uses-permission android:name=“android.permission.POST_NOTIFICATIONS”/>

<strong>&#x3C;application>
</strong>        &#x3C;service
            android:name="com.devtodev.push.internal.logic.DTDFcmMessagingService"
            android:exported="true">
            &#x3C;intent-filter>
                &#x3C;action android:name="com.google.firebase.MESSAGING_EVENT"/>
            &#x3C;/intent-filter>
        &#x3C;/service>

        &#x3C;receiver
            android:name="com.devtodev.push.internal.logic.PushClickReceiver"
            android:enabled="true"
            android:exported="true">
            &#x3C;intent-filter>
                &#x3C;action android:name="com.devtodev.android.push.CLICKED" />
            &#x3C;/intent-filter>
        &#x3C;/receiver>
&#x3C;/application>
</code></pre>

3\. To add a user icon to your push notification and change its color, add the following strings to the manifest file code:

```markup
<meta-data
 android:name="com.devtodev.push.default_small_icon"
 android:resource="@drawable/ic_icon_name" />

<meta-data
 android:name="com.devtodev.push.default_small_icon_color"
 android:resource="@color/icon_color" />
```

To add a large user icon to your push notifications, add:

```markup
<meta-data
android:name="com.devtodev.push.default_large_icon"
android:resource="@mipmap/ic_large_icon_name"/>
```

Example:

```markup
<application
    <!-- Your tags -->
    <service
        android:name="com.devtodev.push.internal.logic.DTDFcmMessagingService">
        <intent-filter>
            <action android:name="com.google.firebase.MESSAGING_EVENT" />
        </intent-filter>
    </service>

    <receiver
        android:name="com.devtodev.push.internal.logic.PushClickReceiver"
        android:enabled="true"
        android:exported="true">
        <intent-filter>
            <action android:name="com.devtodev.android.push.CLICKED" />
        </intent-filter>
    </receiver>

    <meta-data
        android:name="com.devtodev.push.default_small_icon"
        android:resource="@drawable/ic_baseline" />

    <meta-data
        android:name="com.devtodev.push.default_small_icon_color"
        android:resource="@color/colorPrimary" />

    <meta-data
        android:name="com.devtodev.push.default_large_icon"
        android:resource="@mipmap/baseline_accessibility_black_18"/>
</application>
```

4\. After the **`DTDAnalytics`** initializer, add the **`DTDMessaging`** initializer.

Example:&#x20;

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val analyticsConfiguration = DTDAnalyticsConfiguration()
analyticsConfiguration.logLevel = DTDLogLevel.Error

DTDAnalytics.initialize(
    appKey = "projectKey",
    analyticsConfiguration = analyticsConfiguration,
    context = this
)
DTDMessaging.initialize(context = this)
```

{% endtab %}

{% tab title="Java" %}

```java
DTDAnalyticsConfiguration analyticsConfiguration = new DTDAnalyticsConfiguration();
analyticsConfiguration.setLogLevel(DTDLogLevel.Error);
DTDAnalytics.INSTANCE.initialize("projectKey",analyticsConfiguration,context);
DTDMessaging.INSTANCE.initialize(context);
```

{% endtab %}
{% endtabs %}

5\. Subscribe a **`DTDPushListener`** to receive information about the **`DTDMessaging`** functioning.

Example:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
DTDMessaging.setPushListener(object : DTDPushListener {
      override fun onPushServiceRegistrationSuccessful(deviceId: String) {
          //do something
      }
      override fun onPushServiceRegistrationFailed(error: String) {
          //do something
      }
      override fun onPushNotificationReceived(message: Map<String, String?>?) {
          //do something
      }
      override fun onPushNotificationOpened(pushMessage: DTDPushMessage, actionButton: DTDActionButton?) {
          //do something
      }
})
```

{% endtab %}

{% tab title="Java" %}

```java
DTDMessaging.INSTANCE.setPushListener(new DTDPushListener() {
    @Override
    public void onPushServiceRegistrationSuccessful(@NonNull String deviceId) {
        //do something            
    }

    @Override
    public void onPushServiceRegistrationFailed(@NonNull String error) {
        //do something
    }

    @Override
    public void onPushNotificationReceived(@Nullable Map<String, String> message) {
        //do something
    }

    @Override
    public void onPushNotificationOpened(@NonNull DTDPushMessage dtdPushMessage, @Nullable DTDActionButton dtdActionButton) {
        //do something
    }
});
```

{% endtab %}
{% endtabs %}

6\. Call the **`DTDMessaging.startPushService()`** method to activate the Messaging module.

### Android 13 or higher

When using Android 13 or higher, notifications are disabled by default. The app won’t receive notifications until you request a new permission (`POST_NOTIFICATIONS`) and the user grants this permission to your app.

For notifications to work properly, add the following line to the manifest file:

```xml
<uses-permission android:name=“android.permission.POST_NOTIFICATIONS”/>
```

You can check the operation of the `POST_NOTIFICATIONS` permission in your app by inserting this example in the code:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
@RequiresApi(33)
private fun notificationPermissionIsGranted(): Boolean {
    val res = context.checkCallingOrSelfPermission(POST_NOTIFICATIONS)
    return res == PackageManager.PERMISSION_GRANTED
}
```

{% endtab %}

{% tab title="Java" %}

```java
@RequiresApi(33)
private Boolean notificationPermissionIsGranted() {
    int res = context.checkCallingOrSelfPermission(POST_NOTIFICATIONS);
    return res == PackageManager.PERMISSION_GRANTED;
}
```

{% endtab %}
{% endtabs %}

If the check results in a negative answer (access is not granted), call this method:&#x20;

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), yourCode)
```

{% endtab %}

{% tab title="Java" %}

```java
String[] permissionsArr = {Manifest.permission.POST_NOTIFICATIONS};
requestPermissions(permissionsArr, yourCode);
```

{% endtab %}
{% endtabs %}

Its execution will call a dialog that in turn will ask the user to opt in:&#x20;

<figure><img src="/files/Zxb01bLHKKM33pI6LvKb" alt=""><figcaption></figcaption></figure>

This code is going to help you get the user’s decision:&#x20;

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<String?>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (requestCode == yourCode) {
        if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            //permission is granted
        } else {
            //permission is not granted, notifications are not available
        }
    }
}
```

{% endtab %}

{% tab title="Java" %}

```java
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == yourCode) {
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            //permission is granted
        } else {
            //permission is not granted, notifications are not available
        }
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Note. In SDK ver. 2.1.5 or higher, if the permission is not granted, you will see this message in the log from DTDMessaging: “Notifications don’t work. Permission android.permission.POST\_NOTIFICATIONS is not granted”.
{% endhint %}

### External interface of the `DTDMessaging` **module**

| **Object**                                                                                                                                                                         | **Description**                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`DTDMessaging`**                                                                                                                                                                 | The main object for push notification initialization.                                                                                                                                                                                                                                                                                                                                                                                                                        |
| **`DTDMessaging.initialize(Context context)`**                                                                                                                                     | The push notification initialization method.                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| **`DTDMessaging.startPushService()`**                                                                                                                                              | The push notification activation method. It passes the **`isAllowed`** current state.                                                                                                                                                                                                                                                                                                                                                                                        |
| **`DTDMessaging.pushNotificationsAllowed`** = true or false                                                                                                                        | <p>A property responsible for the activation/deactivation of push notifications.<br></p><p>Functions as a getter (describes the current state) and a setter (sets the current state).<br></p><p>When the state transitions, it sends a pt with <strong><code>isAllowed</code></strong> (<em><strong>true</strong></em> or <em><strong>false</strong></em>) status to the server.<br></p><p>The <strong><code>isAllowed</code></strong> flag status is stored in the SDK.</p> |
| **`DTDMessaging.setIntent(Intent intent)`**                                                                                                                                        | A method of passing a user intent to the SDK using PushMessage.                                                                                                                                                                                                                                                                                                                                                                                                              |
| <p>Written in the manifest file<br><strong><code>\<meta-data android:name="com.devtodev.push.default\_small\_icon" android:resource="@drawable/smallIcon" /></code></strong></p>   | Sets a small custom user icon.                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| <p>Written in the manifest file<br><strong><code>\<meta-data android:name="com.devtodev.default\_small\_icon\_color" android:resource="@color/colorPrimary" /></code></strong></p> | Sets a color of the small custom user icon.                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| <p>Written in the manifest file<br><strong><code>\<meta-data android:name="com.devtodev.push.default\_large\_icon" android:resource="@mipmap/largeIcon" /></code></strong></p>     | Sets a large custom user icon.                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| **`DTDMessaging.getToken()`**                                                                                                                                                      | Returns a push notification registration token (**`firebaseToken`**).                                                                                                                                                                                                                                                                                                                                                                                                        |
| **`DTDMessaging.processPushNotification(Context context, RemoteMessage remoteMessage)`**                                                                                           | Used to pass the push notification to the FirebaseMessagingService if it was implemented by the client but not by the SDK.                                                                                                                                                                                                                                                                                                                                                   |
| **`DTDMessaging.setPushListener(DTDPushListener pushListener)`**                                                                                                                   | (**`DTDPushListener pushListener`**) - sets a listener for push notification event trapping.                                                                                                                                                                                                                                                                                                                                                                                 |

### `DTDPushListener` Interface Methods

| DTDPushListener Interface Methods                                                                  | Description                                                           |
| -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| **`onPushServiceRegistrationSuccessful(String deviceId)`**                                         | Returns a push notification registration token (**`firebaseToken`**). |
| **onPushServiceRegistrationFailed(String error)**                                                  | Returns errors during push notification registration.                 |
| **`onPushNotificationReceived(Map<String, String> message)`**                                      | Returns a directory with data for improving your push notifications.  |
| **`onPushNotificationOpened(DTDPushMessage pushMessage, @Nullable DTDActionButton actionButton)`** | Returns **`pushMessage`** and **`actionButton`** if they were tapped. |

### A Class for Receiving Notification Data (**`DTDPushMessage`**).

#### Basic Class Properties

| Property                                       | Type                   | Description                                                                                                                                                                                                                                                                                        |
| ---------------------------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`getData()`**                                | Map\<String, String>   | Complete information sent with the use of a remote push notification.                                                                                                                                                                                                                              |
| **`systemId`**                                 | Int                    | The notification ID used in the devtodev system.                                                                                                                                                                                                                                                   |
| **`getTitle(context:Context)`**                | String?                | Returns the selected message title or app name if the former is unavailable.                                                                                                                                                                                                                       |
| **`body`**                                     | String?                | The text body.                                                                                                                                                                                                                                                                                     |
| **`group`**                                    | String?                | A group of messages.                                                                                                                                                                                                                                                                               |
| **`getSound(context: Context)`**               | Uri?                   | Returns the storage path of an audio file.                                                                                                                                                                                                                                                         |
| **`getSoundName()`**                           | String?                | The notification sound name.                                                                                                                                                                                                                                                                       |
| **`tag`**                                      | String?                | The notification tag.                                                                                                                                                                                                                                                                              |
| **`color`**                                    | String?                | The notification color.                                                                                                                                                                                                                                                                            |
| **`bigPicture`**                               | String?                | The notification banner if specified.                                                                                                                                                                                                                                                              |
| **`actionType`**                               | DTDActionType          | <p>The property that returns an enum’s DTDActionType value.<br></p><p>Possible values:</p><ul><li><em><strong>Url</strong></em> - an external link opening</li><li><em><strong>Share</strong></em> - content sharing</li><li><em><strong>Deeplink</strong></em> - an in-app link opening</li></ul> |
| **`actionString`**                             | String?                | The property that returns an optional action ID.                                                                                                                                                                                                                                                   |
| **`getIcon(context: Context, userIcon: Int)`** | Int                    | The icon resource identifier specified by the user (if specified).                                                                                                                                                                                                                                 |
| **`largeIcon`**                                | String?                | The large notification icon name.                                                                                                                                                                                                                                                                  |
| **`actions`**                                  | List\<DTDActionButton> | The list of action buttons used in the push notification.                                                                                                                                                                                                                                          |
| **`isApiSource`**                              | Boolean                | Specifies whether the push notification was sent using the devtodev API.                                                                                                                                                                                                                           |

### A Class for Handling the Notification Button Taps (**`DTDActionButton`**)

| Property           | Type          | Description                                                                                                                                                                                                                                                                                                                                                                              |
| ------------------ | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`Id`**           | String?       | The property that returns the tapped button ID.                                                                                                                                                                                                                                                                                                                                          |
| **`actionString`** | String?       | The property that returns the optional action ID.                                                                                                                                                                                                                                                                                                                                        |
| **`actionType`**   | DTDActionType | <p>The property that returns an enum’s <strong><code>DTDActionType</code></strong> value.<br></p><p>Possible values:</p><ul><li><em><strong>App</strong></em> - a default value</li><li><em><strong>Url</strong></em> - an external link opening</li><li><em><strong>Share</strong></em> - content sharing</li><li><em><strong>Deeplink</strong></em> - an in-app link opening</li></ul> |
| **`icon`**         | String?       | The property that returns the button’s icon name.                                                                                                                                                                                                                                                                                                                                        |
| **`isBackground`** | Boolean       | The button-click app open mode.                                                                                                                                                                                                                                                                                                                                                          |
| **`text`**         | String?       | The property that returns the text of the tapped button.                                                                                                                                                                                                                                                                                                                                 |


# iOS

## Platform integration

#### Creating a Universal Push Notification Client SSL Certificate

You use Member Center to generate a push notification client SSL certificate that allows your notification server to connect to the APNs. Each App ID is required to have its own client SSL certificate. The client SSL certificate Member Center generates is a universal certificate that allows your app to connect to both the development and production environments.

{% hint style="info" %}
Only a team agent or admin can generate Apple Push Notification service SSL certificates.
{% endhint %}

To generate a universal client SSL certificate

1. In [Certificates, Identifiers & Profiles](http://developer.apple.com/account), select Certificates.
2. Click the Add button (+)<br>

   <figure><img src="/files/NKMEsJ3dUEKX0RDJJ1PE" alt=""><figcaption></figcaption></figure>
3. Under Production, select the “Apple Push Notification service SSL (Sandbox & Production)” checkbox, and click Continue.<br>

   <figure><img src="/files/DiKWIN1Yq8A9cqVn2JPO" alt=""><figcaption></figcaption></figure>
4. Choose an App ID from the App ID pop-up menu, and click Continue. Choose the explicit App ID that matches your bundle ID.
5. Create a certificate request on your Mac.
6. Click Choose File.
7. In the dialog that appears, select the certificate request file (with a .certSigningRequest extension), and click Choose File.
8. Click Generate.
9. Click Download.<br>

   <figure><img src="/files/Cl40qIyb8iMvhQIRfaKx" alt=""><figcaption></figcaption></figure>

#### **Follow these steps to export the certificate from Apple web-site to the P12-file:**

1. Open "Keychain access" application
2. If the certificate hasn't been added to keychain access yet, choose "File" →  "Import". Find the certificate file (CER-file) provided by Apple
3. Choose "Keys" section in "Keychain access" application
4. Choose a personal key associated with your iPhone developer certificate. Personal key is identified by open certificate associated with it "iPhone developer: ". Choose "File" → Export objects. Save key as .p12
5. You'll be suggested to create a password which is used when you need to import the key to another computer

#### Upload the certificate to the site

Upload the .p12-file into Integration section of application settings panel  (Settings -> Push Notifications):

![](/files/fUHlyrKAPgkp4UD25veB)

## Messaging Module Integration (CocoaPods)

[CocoaPods](http://cocoapods.org/) is the easiest way to add devtodev into your iOS project.

1\. Firstly, install CocoaPods using:

```bash
sudo gem install cocoapods
```

2\. In the project directory execute the command:

```bash
pod init
```

3\. In the created Podfile add the dependency:

```bash
platform :ios, '9.0'

target 'TargetName' do
  use_frameworks!
  pod 'DTDAnalytics', '~> 2.0.0'
  pod 'DTDMessaging', '~> 2.0.0'
end
```

4\. Finally, run the command in your Xcode project directory:

```bash
pod install
```

CocoaPods should download and install the devtodev library, and create a new Xcode workspace. Open this workspace in Xcode.

## Messaging Module Integration (Manual Installation) <a href="#messaging-module-integration-manual-installation" id="messaging-module-integration-manual-installation"></a>

To connect the module for processing push notifications, you need to:

1. [Download the latest devtodev SDK from the repository](https://github.com/devtodev-analytics/ios-sdk-2.0).
2. Add **`DTDAnalytics.xcframework`** to the project (check Do Not Embed).

{% hint style="info" %}
The DTDMessaging plugin will not work without the DTDAnalytics main analytics plugin.
{% endhint %}

3. Add DTDMessaging.xcframework to the project (check Do Not Embed)

![](/files/-MgQXvnqMCh2mAaDFPq5)

4. In the Xcode project settings, open the tab, and add: "Push Notifications" and "Background Modes" respectively.

<div align="left"><img src="/files/-MgQY2khXyJOV5Qp4GUY" alt=""></div>

<div align="left"><img src="/files/-MgQYAnJJ-kWXw_hT5Uq" alt=""></div>

5. In the "Background Modes" section, enable "Remote notifications".

![](/files/-MgQYTYlJo8Rc9oWkzfF)

6. Add initialization to **`didFinishLaunchingWithOptions`** method:

{% tabs %}
{% tab title="Swift" %}

```swift
let config = DTDAnalyticsConfiguration()
config.logLevel = .error
DTDAnalytics.initialize(applicationKey: "App ID", configuration: config)

DTDMessaging.delegate = self
DTDMessaging.pushNotificationsOptions = [.DTDNotificationOptionAlert,
                                         .DTDNotificationOptionSound,
                                         .DTDNotificationOptionBadge]
DTDMessaging.startPushService()
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
DTDNotificationOptions *options = [[DTDNotificationOptions alloc] initWithRawValue:
                                  [DTDNotificationOptions.DTDNotificationOptionAlert rawValue] |
                                  [DTDNotificationOptions.DTDNotificationOptionSound rawValue] |
                                  [DTDNotificationOptions.DTDNotificationOptionBadge rawValue]];
[DTDMessaging setPushNotificationsOptions:options];
[DTDMessaging startPushService];
```

{% endtab %}
{% endtabs %}

7. To handle SDK delegate methods, you need to add the implementation of the **`DTDMessagingDelegate`** protocol.

{% tabs %}
{% tab title="Swift" %}

```swift
extension AppDelegate: DTDMessagingDelegate {
    func didRegisterForRemoteNotifications(with deviceToken: Data) {
        // your code
    }

    func didFailToRegisterForRemoteNotifications(with error: Error) {
        // your code
    }

    func didReceiveInvisibleNotification(with message: DTDMessage) {
        // your code
    }

    func didReceiveForegroundNotification(with message: DTDMessage) {
        // your code
    }

    func didOpenRemoteNotification(with message: DTDMessage, and buttonClicked: DTDActionButton?) {
        // your code
    }
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
@interface AppDelegate () <DTDMessagingDelegate>

- (void)didRegisterForRemoteNotificationsWith:(NSData *)deviceToken {
    // your code
}

- (void)didFailToRegisterForRemoteNotificationsWith:(NSError *)error {
    // your code
}

-(void)didReceiveInvisibleNotificationWith:(DTDMessage *)message {
    // your code
}

- (void)didReceiveForegroundNotificationWith:(DTDMessage *)message {
    // your code
}

-(void)didOpenRemoteNotificationWith:(DTDMessage *)message and:(DTDActionButton *)buttonClicked {
    // your code
}
```

{% endtab %}
{% endtabs %}

The **`DTDMessaging`** module provides support for notifications with attachments. These notifications are available since iOS 10. Attachments support images, animated gifs and videos. To use this function, you will need to create a “**Notification Service Extension**“, for this create a new target in your application settings:

* Open Xcode (File -> New -> Target).\
  Select ***Notification Service Extension***.

![](/files/-MgQZPRZ-Bq5mrs35P1G)

The next step is to modify the **`Notification Extension`** class as follows:

* Delete all auto-generated code.
* Inherit **`Notification Extension`** class from **`DTDMediaAttachmentExtension`**

Example:

```swift
import UserNotifications
import DTDMessaging

class NotificationService: DTDMediaAttachmentExtension {

}
```

### External interface of the `DTDMessaging` module <a href="#external-interface-of-the-dtdmessaging-module" id="external-interface-of-the-dtdmessaging-module"></a>

<table data-header-hidden><thead><tr><th width="237.21311475409834">Object</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><strong>Object</strong></td><td>Type</td><td><strong>Description</strong></td></tr><tr><td><strong><code>startPushService</code></strong></td><td></td><td><p>Method responsible for activating push notifications:</p><ul><li>Requests permission from the user to receive Push Notifications</li><li>Sends a pushToken and the current state <em><strong><code>isAllowed</code></strong></em></li></ul></td></tr><tr><td><strong><code>apnsToken</code></strong><br></td><td>Data?</td><td>Getter giving the current pushToken, represented by a Data </td></tr><tr><td><strong><code>apnsTokenString</code></strong></td><td>String?</td><td>Getter giving the current pushToken,  represented by a String </td></tr><tr><td><strong><code>delegate</code></strong></td><td>DTDMessagingDelegate?</td><td>Property for assigning a delegate to handle events from the SDK </td></tr><tr><td><strong><code>pushNotificationsOptions</code></strong></td><td>DTDNotificationOptions</td><td><p>Configuring the display of Push Notifications, is an <strong><code>OptionSet</code></strong>, is set by the developer to select the method for notifying the user. May be changed by the end user.</p><p>By default it has the following value:  <em><strong><code>[.DTDNotificationOptionBadge, .DTDNotificationOptionSound, .DTDNotificationOptionAlert]</code></strong></em> </p></td></tr></tbody></table>

### Optional `DTDMessagingDelegate` methods <a href="#optional-dtdmessagingdelegate-methods" id="optional-dtdmessagingdelegate-methods"></a>

| **Delegate method**                                                                                                  | **Description**                                                                                                                                                   |
| -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`didFailToRegisterForRemoteNotifications`**`(with error:`` `**`Error`**`)`                                         | The method is called when an error occurs at the time of receiving a pushToken, represented by the **`Error`** base class.                                        |
| **`didOpenRemoteNotification`**`(with message:`` `**`DTDMessage`**`, and buttonClicked:`` `**`DTDActionButton?`**`)` | The method is called when a remote push notification is opened by an end user. A **`DTDMessage`** object and an optional **`DTDActionButton`** object are passed. |
| **`didReceiveForegroundNotification`**`(with message:`` `**`DTDMessage`**`)`                                         | The method is called when a remote push notification is received when the application is in the ***Foreground*** state. The **`DTDMessage`** object is passed.    |
| **`didReceiveInvisibleNotification`**`(with message:`` `**`DTDMessage`**`)`                                          | The method is called when an invisible remote push notification is received. The **`DTDMessage`** object is passed.                                               |
| **`didRegisterForRemoteNotifications`**`(with deviceToken:`` `**`Data`**`)`                                          | The method is called in case the **`pushToken`** successfully arrives; represented by a Data.                                                                     |

### Class for receiving Notification data (**DTDMessage**)  <a href="#class-for-receiving-notification-data-dtdmessage" id="class-for-receiving-notification-data-dtdmessage"></a>

| **Property**       | Type               | **Description**                                                                                                                                                                                                                                                                                                                                                                                                     |
| ------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`payload`**      | \[AnyHashable:Any] | Full information sent using remote push notification.                                                                                                                                                                                                                                                                                                                                                               |
| **`actionType`**   | DTDActionType      | <p>Property returning the enum <strong>DTDActionType</strong> value. Possible values:</p><ul><li><em><strong><code>App</code></strong></em> - default value</li><li><em><strong><code>Url</code></strong></em> - open an external link</li><li><em><strong><code>Share</code></strong></em> - share content</li><li><em><strong><code>Deeplink</code></strong></em> - open a link inside the application </li></ul> |
| **`actionString`** | String?            | Property returning an optional action identifier.                                                                                                                                                                                                                                                                                                                                                                   |
| **`badge`**        | Int                | The value that is passed to display a *badge* on the application icon.                                                                                                                                                                                                                                                                                                                                              |
| **`category`**     | String?            | Property returning an optional identifier of a push notification category                                                                                                                                                                                                                                                                                                                                           |

### Class for handling pressed buttons on a notification (**DTDActionButton**) <a href="#class-for-handling-pressed-buttons-on-a-notification-dtdactionbutton" id="class-for-handling-pressed-buttons-on-a-notification-dtdactionbutton"></a>

| **Property**       | Type          | **Description**                                                                                                                                                                                                                                                                                                                                                                                                    |
| ------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`actionType`**   | DTDActionType | <p>Property returning the enum <strong>DTDActionType</strong> value. Possible values:</p><ul><li><em><strong><code>App</code></strong></em> - default value</li><li><em><strong><code>Url</code></strong></em> - open an external link</li><li><em><strong><code>Share</code></strong></em> - share content</li><li><em><strong><code>Deeplink</code></strong></em> - open a link inside the application</li></ul> |
| **`actionString`** | String?       | Property returning an optional action identifier.                                                                                                                                                                                                                                                                                                                                                                  |
| **`buttonId`**     | String        | Property returning the identifier of the pressed button.                                                                                                                                                                                                                                                                                                                                                           |
| **`text`**         | String        | Property returning the text of the pressed button.                                                                                                                                                                                                                                                                                                                                                                 |

## Notes <a href="#notes" id="notes"></a>

### Foreground Notification <a href="#foreground-notification" id="foreground-notification"></a>

Displaying push notifications in **Foreground** state is available since iOS 10.0.

By default, according to Apple Push Notification Guidelines, the display of push notification in the **Foreground** state is disabled. In order to set the display method, you must:

* Assign delegate for **`UNUserNotificationCenter`**
* Or pass a parameter in the push notification constructor (***`_fg = 1`***). In this case, the display method will be formed from the Notification settings.
* Or delegate the system method **`userNotificationCenter willPresent notification`**. In this case, the display properties will be taken from the developer.

An example of a delegated method implementation:&#x20;

```swift
func userNotificationCenter(_ center: UNUserNotificationCenter, 
                            willPresent notification: UNNotification, 
                            withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
  completionHandler([.alert, .sound])
}
```

### DTDNotificationOptions <a href="#dtdnotificationoptions" id="dtdnotificationoptions"></a>

It is an **`OptionSet`** that is used to pass push notification authorization and set up interactions with users.

{% hint style="info" %}
The user can change the allowed parameters at any time in the notification settings.
{% endhint %}

Possible values:

| **Value**                                     | Description                                                                                                                                                            |
| --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ***`DTDNotificationOptionBadge`***            | display a badge on the application icon                                                                                                                                |
| ***`DTDNotificationOptionSound`***            | play sound                                                                                                                                                             |
| ***`DTDNotificationOptionAlert`***            | display an alert                                                                                                                                                       |
| ***`DTDNotificationOptionCarPlay`***          | display push notification in CarPlay                                                                                                                                   |
| ***`DTDNotificationOptionCriticalAlert`***    | play sound for critical notifications regardless of the “Do Not Disturb” setting (*Critical alerts require special permission from Apple*.) *Available since iOS 12.0* |
| ***`DTDNotificationOptionProvidesSettings`*** | an option specifying that the system should display a button for notification settings in the application. *Available since iOS 12.0*                                  |
| ***`DTDNotificationOptionProvisional`***      | pre-send notifications to the Action Center without interrupting work. *Available since iOS 12.0*                                                                      |
| ***`DTDNotificationOptionAnnouncement`***     | for Siri to automatically read messages through AirPods. *Available since iOS 13.0*                                                                                    |

{% hint style="info" %}
***`DTDNotificationOptionProvisional`*** - Provisional push notifications appear in the user's Notification Center, but not on the lock screen. This type of push notification does not have to be explicitly allowed by the user. Start submitting them as soon as the user installs and runs your application. However, the user can also opt in/opt out of notifications, but they will need to do it explicitly.

This setting is used to prevent receiving push notification permission requests at the start, which is intrusive and most users refuse to receive them.

It is also important that when using the ***`DTDNotificationOptionProvisional`*** setting, the user will be able to subscribe to explicit notifications only from the notification center settings.
{% endhint %}

### **`DTDMediaAttachmentExtension`** <a href="#dtdmediaattachmentextension" id="dtdmediaattachmentextension"></a>

To handle the application attitude to displaying push notifications, you need to add a Notifications service Extension. And inherit **`NotificationService`** from **`DTDMediaAttachmentExtension`**

```kotlin
@available(iOSApplicationExtension 10.0, *)
class NotificationService: DTDMediaAttachmentExtension {
  // nothing
}
```

### Custom sounds

To use custom sounds for push notifications, you need to copy the necessary sound files to the Libraries folder of your Xcode project. You can read more about supported formats [here](https://developer.apple.com/documentation/usernotifications/unnotificationsound#2943048).

<figure><img src="/files/qrdGlRhx7bX4cUcYL32h" alt=""><figcaption></figcaption></figure>

When creating a push company, it is important to specify the full name of the sound with file extension (for example, sound.wav).

### DTDMessaging integration with swizzling disabled

DTDMessaging automatically swizzles notification tracking and APNS-token usage methods by default. If you want to disable the function, add the flag **`DTDMessagingSwizzlingEnabled`** (boolean) in the app’s **`Info.plist`** file and set it to ***NO (0)***. However, if you disable notification swizzling, you will need additional integration for accurate analytics:

For sending APNS-token to DTDMessaging:

{% tabs %}
{% tab title="Swift" %}

```swift
func application(_ application: UIApplication, 
                didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
  DTDMessaging.apnsToken = deviceToken
}i
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    [DTDMessaging setApnsToken:deviceToken];
}
```

{% endtab %}
{% endtabs %}

For sending information about the application:

{% tabs %}
{% tab title="Swift" %}

```swift
func application(_ application: UIApplication,
                didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
  DTDMessaging.didReceiveMessage(userInfo: userInfo, actionIdentifier: nil)
}

@available(iOS 10.0, *)
extension AppDelegate: UNUserNotificationCenterDelegate {
  func userNotificationCenter(_ center: UNUserNotificationCenter, 
                              didReceive response: UNNotificationResponse, 
                              withCompletionHandler completionHandler: @escaping () -> Void) {
    let userInfo = response.notification.request.content.userInfo
    let actionIdentifier = response.actionIdentifier
    DTDMessaging.didReceiveMessage(userInfo: userInfo, actionIdentifier: actionIdentifier)
  }

  func userNotificationCenter(_ center: UNUserNotificationCenter, 
                              willPresent notification: UNNotification, 
                              withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    let userInfo = notification.request.content.userInfo
    DTDMessaging.willPresentMessage(userInfo: userInfo)
  }
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
    [DTDMessaging didReceiveMessageWithUserInfo:userInfo actionIdentifier:nil];
}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
    NSDictionary *userInfo = notification.request.content.userInfo;
    [DTDMessaging willPresentMessageWithUserInfo:userInfo];
}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler {
    NSDictionary *userInfo = response.notification.request.content.userInfo;
    NSString *actionIdentifier = response.actionIdentifier;

    [DTDMessaging didReceiveMessageWithUserInfo:userInfo actionIdentifier:actionIdentifier];
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
Note: If you disable automatic notification swizzling, **`DTDMessagingDelegate`** methods will not be called.
{% endhint %}


# Windows (UWP)

## Push Notifications (UWP)

The **`DevToDev.Messaging`** package necessary for notifications is available in the **`NuGet`** package manager.

### 1. NuGet Installation

{% embed url="<https://www.nuget.org/packages/DevToDev.Analytics.Uwp/>" %}

{% embed url="<https://www.nuget.org/packages/DevToDev.Messaging.Uwp/>" %}

**Package Manager UI**

Find the **`DevToDev.Messaging`** package using the package manager search engine and click **Install**. The latest version of the package is recommended.

### 2. Credentials

To integrate WNS, you need to get the Package SID and Application Secret Key from the [Microsoft Partner Center](https://partner.microsoft.com/en-us/dashboard/home) and specify them in the devtodev push notification settings (Project -> Settings-> Push notifications):

![](/files/DLCSY7rQHycVNBfEE4YQ)

![](/files/10U3QFb39qFPFqedjAmS)

![](/files/ATo820UZHzGVu1eT2lgS)

![](/files/6zulZMLH95MGPBvN9eIG)

### 3. Setup

For the correct package functioning, add handlers invoke to the **`Windows.UI.Xaml.Application`** class implementation.

```csharp
sealed partial class App : Application
{
  protected override void OnLaunched(LaunchActivatedEventArgs e)
  {
    // Your code...
    DTDMessaging.HandleActivatedEvent(e);
  }
  
  protected override void OnActivated(IActivatedEventArgs args)
  {
      // Your code...
      DTDMessaging.HandleActivatedEvent(args);
  }
}
```

Besides, in the UI editor of the ***Package.appxmanifest*** file do the following:

1. Add Background Tasks to the Declarations tab and mark it as System Event. After that enter **`DevToDev.Background.ToastNotificationBackgroundTask`** to the Entry Point field.
2. Add Background Tasks to the Declarations tab and mark it as Push Notification. After that enter **`DevToDev.Background.RawNotificationBackgroundTask`** to the Entry Point field.

### 4. Initialization

For the **`DevToDev.Messaging`** package functioning you need to have the main **`DevToDev.Analytics`** package installed. Initialize the SDK before initializing messages. You can read about it in more detail in the [SDK initialization section](https://docs.devtodev.com/integration/integration-of-sdk-v2/push-notifications/pages/-MkwFB-F75tzaUO1ocsq#id-2.-sdk-initialization).

After the SDK has been initialized you can move to initializing messages. To do this, call the method:

```csharp
DTDMessaging.SetMessagingEnabling(true);
```

### 5. Events

It is possible to listen to events from the **`DevToDev.Messaging`** package:

1\. Push Token - a string that allows to identify the client on a remote server for sending him customized notifications. For listening the unique Push Token ID issue event, it is necessary to be subscribed to the event:

```csharp
DTDMessaging.OnTokenReceived += token => { /* Your code... */ };
```

2\. To track the errors related to the unique Push Token ID issue, it is necessary to be subscribed to the event:

```csharp
DTDMessaging.OnTokenFailed += error => { /* Your code... */ };
```

Where error is a string value containing information about the error.

3\. To handle incoming message data, it is necessary to be subscribed to the following event:

```csharp
DTDMessaging.OnMessageReceived += messageData => { /* Your code... */ };
```

Where **`messageData`** belongs to the **`IDictionary<string, string>`** type and contains data sent from the server together with the message.

4\. To handle notification activation events, it is necessary to be subscribed to the event:

```csharp
DTDMessaging.OnMessageActivated += messageAction => { /* Your code with token. */ };
```

Where **`messageAction`** is **`DevToDev.Messaging.DTDMessageAction`** class instance:

```csharp
/// <summary>
/// The class contains information about notification's action.
/// </summary>
public sealed class DTDMessageAction
{
    /// <summary>
    /// Action type.
    /// Can be: Open, Url, Share, DeepLink.
    /// </summary>
    public DTDMessageActionType ActionType { get; }

    /// <summary>
    /// Action string.
    /// </summary>
    public string ActionString { get; }

    /// <summary>
    /// Activated button ID.
    /// </summary>
    public string ButtonId { get; }

    /// <summary>
    /// Activated button's text.
    /// </summary>
    public string ButtonText { get; }

    /// <summary>
    /// Notification data.
    /// </summary>
    public IReadOnlyDictionary<string, string> MessageData { get; }
  }
```

### **6.** Disabling

Call the method to turn notifications off:

```csharp
DTDMessaging.SetMessagingEnabling(false);
```


# Unity

## General Information

In order to work with push notifications, you need to integrate the *messaging* module. You can do it by using one of the two methods: with the help of the *Unity Package Manager* (recommended) or by manually importing the *unitypackage*.

### **Integration by using the Unity Package Manager**

{% hint style="warning" %}
If you integrated the devtodev package manually, then you need to delete the Assets/DevToDev and Plugins/DevToDev folders.
{% endhint %}

1. In the Package Manager (Window → Package Manager), click + in the top left corner and select *Add package from git UR*L.
2. Copy the repository URL <https://github.com/devtodev-analytics/package_Messaging.git> to the input box and click *Add*.

   <figure><img src="/files/yhdO04ho426S060Sxib4" alt=""><figcaption></figcaption></figure>
3. Wait for the Unity Package Manager to download the package and all the necessary dependencies.
4. If you use Android, allow dependencies in Assets → External Dependency Manager → Android Resolver → Resolve.

{% hint style="info" %}
You can pick a specific SDK version by adding # and a version number at the end of the URL, for example: <https://github.com/devtodev-analytics/package_Messaging.git#v3.3.2>
{% endhint %}

### **Integration by importing&#x20;*****unitypackage***

1. Download the latest version of devtodev package from the repository: <https://github.com/devtodev-analytics/Unity-sdk-3.0/releases/latest>
2. Import DTDAnalytics.unitypackage to your project
3. Import DTDMessaging.unitypackage to your project
4. If you use Android, allow dependencies in Assets → External Dependency Manager → Android Resolver → Resolve.

![](https://lh3.googleusercontent.com/_SdzNiRPJM_bi6HwZdR4zp_GqjKJELgPARR3aXNmsfATEVswaIgwtKsaTxTm-nVzRX16lINih8VZ_aKoVk5ThzsdBBbTXeGUQum94L35DDexbRrVd2HFNSpeDL0NXeKc4C8Di9UQ=s0)

## Platform specific integration

{% content-ref url="/pages/6s5F6vkBrtmBCeGbvaNx" %}
[Android](/integration/integration-of-sdk-v2/push-notifications/unity/android)
{% endcontent-ref %}

{% content-ref url="/pages/7S0Kv77M1UHQPsWScp03" %}
[iOS](/integration/integration-of-sdk-v2/push-notifications/unity/ios)
{% endcontent-ref %}

{% content-ref url="/pages/odw2uYQyQmuUpk4cXj73" %}
[Windows (UWP/WSA)](/integration/integration-of-sdk-v2/push-notifications/unity/windows-uwp-wsa)
{% endcontent-ref %}


# Android

Android Integration

## Platform integration

Push Notifications on Android are sent with the help of the FCM service.

You can find instructions on how to create a project in Firebase and integrate Firebase Services into your application in the [Firebase documentation](https://firebase.google.com/docs/android/setup).

In devtodev (Settings → Push notifications → Push notifications panel), specify the Firebase project ID and authorize devtodev to send messages and manage messaging subscriptions for your Firebase application.&#x20;

<figure><img src="/files/GIFXIrjC3C991Y4JhH4L" alt=""><figcaption></figcaption></figure>

To get the Firebase project ID, go to the Project Settings → General of your Android project in the Firebase Console. Copy the ID and paste it in devtodev settings.

![](/files/2IUOFxFGnwAM4mu79DoY)

Download google-services.json and put it to Assets folder.

![](/files/52S4kycyMtpRmwYhPfT9)

## Module initialization

1\. For the Messaging module to function you need the basic Analytics package. Before the notification initialization, you need to initialize the SDK. More about it you can read here: [Unity Integration](/integration/integration-of-sdk-v2/sdk-integration/unity).

2\. After the DTDAnalytics initialization block add:

```csharp
DTDMessaging.Android.Initialize();
```

3\. To activate the Messaging module, call :

```csharp
DTDMessaging.Android.StartPushService();
```

Optional:

You can listen to basic notification module events. To do this, create a class that implements the **`IDTDPushListener`** interface and pass it to the **`DTDMessaging.Android.SetPushListener`** method.

Example of the class:

```csharp
public class MyPushListener : IDTDPushListener
{
    public void OnPushServiceRegistrationSuccessful(string deviceId)
    {
        Debug.Log(deviceId);
    }

    public void OnPushServiceRegistrationFailed(string error)
    {
        Debug.Log(error);

    }

    public void OnPushNotificationReceived(DTDPushMessage message)
    {
        Debug.Log(message);
    }

    public void OnInvisibleNotificationReceived(DTDPushMessage message)
    {
        //IOS only.
    }

    public void OnPushNotificationOpened(DTDPushMessage pushMessage, DTDActionButton actionButton)
    {
        Debug.Log(pushMessage.ToString());
        Debug.Log(actionButton.ToString());
    }
}
```

Full example of notification module initialization:

```csharp
public class MyPushListener : IDTDPushListener
{
    public void OnPushServiceRegistrationSuccessful(string deviceId)
    {
        Debug.Log(deviceId);
    }

    public void OnPushServiceRegistrationFailed(string error)
    {
        Debug.Log(error);
    }

    public void OnPushNotificationReceived(DTDPushMessage message)
    {
        Debug.Log(message);
    }

    public void OnInvisibleNotificationReceived(DTDPushMessage message)
    {
        Debug.Log(message);
    }

    public void OnPushNotificationOpened(DTDPushMessage pushMessage, DTDActionButton actionButton)
    {
        Debug.Log(pushMessage.ToString());
        Debug.Log(actionButton.ToString());
    }
}

public class NotificationExample : MonoBehaviour
{
    private const string APP_KEY = "***************"
    void Start()
    {
        DTDAnalytics.Initialize(APP_KEY);
        DTDMessaging.Android.SetPushListener(new PushListener());
        DTDMessaging.Android.Initialize();
        DTDMessaging.Android.StartPushService();
    }
}
```

### Set a custom icon and sound for push notifications

#### Unity 2019 and older

To set a custom sound, icon and its colour in a push notification, copy icons and sounds files in the `Assets/Plugins/Android/res/` folder and add the following strings to the manifest file code:

```csharp
<meta-data
 android:name="com.devtodev.push.default_small_icon"
 android:resource="@drawable/ic_icon_name" />

<meta-data
 android:name="com.devtodev.push.default_small_icon_color"
 android:resource="@color/icon_color" />
```

To set a large user icon in the push notification, add:

```csharp
<meta-data
android:name="com.devtodev.push.default_large_icon"
android:resource="@mipmap/ic_large_icon_name"/>
```

#### Unity 2020 and newer

* Delete the ***`\Assets\Plugins\Android\res`*** folder (together with the ***`.meta`*** file) - it will cause an error during assembly.
* Create a new folder in a separate folder outside of the project.
* In the new folder, create ***`AndroidManifest.xml`*** with the following content (replace **`company`** and **`package`** with you own names)

```csharp
<manifest package="com.company.package">
</manifest>
```

* Create a res folder in the same folder.
* Add your resources to the res folder while keeping the folder structure intact (drawable, xml, raw, etc.). An example of the resulting structure:

```
├─── AndroidManifest.xml
└─── res
    └─── drawable
        └─── smallIcon0.png
     └─── mipmap
        └─── largeIcon0.png
     └─── raw
        └─── iconsound.wav 
```

* Run the following code in the in the command line/terminal:

```
jar cvf resources.aar -C . .
```

* Place the resulting aar file to the ***`\Assets\Plugins\Android\ (edited)`*** folder
* Add the following strings to the project’s Android manifest file ( ***`\Assets\Plugins\Android\AndroidManifest.xml`***):

```csharp
<meta-data android:name="com.devtodev.push.default_small_icon" android:resource="@drawable/smallIcon0" />
<meta-data android:name="com.devtodev.push.default_large_icon" android:resource="@mipmap/largeIcon0" />
```

### External interface of the DTDMessaging module for Android platform

| Method                                                                                                  | Description                                                                                                                                                                                                                                                                                                                                              |
| ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`void DTDMessaging.Android.Initialize()`**                                                            | The push notification initialization method                                                                                                                                                                                                                                                                                                              |
| **`void DTDMessaging.Android.StartPushService()`**                                                      | The push notification activation method. It passes the **`isAllowed`** current state                                                                                                                                                                                                                                                                     |
| **`void DTDMessaging.Android.PushIsAllowed (bool isAllowed)`**                                          | <p>A property responsible for the activation/deactivation of push notifications.When the state transitions, it sends a pt with <strong><code>isAllowed</code></strong> (<em><strong>true</strong></em> or <em><strong>false</strong></em>) status to the server.</p><p>The <strong><code>isAllowed</code></strong> flag status is stored in the SDK.</p> |
| **`void DTDMessaging.Android.GetPushState(Action<bool?> onGetPushState)`**                              | The method that returns the push module state to **`onGetPushState`** callback. If getting the current state is impossible, it returns null                                                                                                                                                                                                              |
| **`void DTDMessaging.Android.GetToken(Action<string> onGetToken)`**                                     | The method that returns push registration token to onGetToken callback                                                                                                                                                                                                                                                                                   |
| **`void DTDMessaging.Android.ProcessPushNotification (IDictionary<string, string> firebaseMessaging)`** | Used to pass the push notification to the **`FirebaseMessagingService`** if it was implemented by the client but not by the SDK                                                                                                                                                                                                                          |
| **`void DTDMessaging.SetPushListener (IDTDPushListener pushListener)`**                                 | It sets a listener for push notification event trapping                                                                                                                                                                                                                                                                                                  |

### Class for receiving Notification data (***`DTDPushMessage`***). Main class properties

| Property                                          | Description                                                                                                                                                                                                                                                                                                                                              |
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`IDictionary<string,string> GetData():`**       | Complete information passed with the push notification.                                                                                                                                                                                                                                                                                                  |
| **`DTDActionType ActionType:`**                   | <p>The property that returns the value of enum’s <strong><code>DTDActionType</code></strong>.</p><p>Possible values:</p><p><em><strong>App</strong></em> - app open</p><p><em><strong>Url</strong></em> - external link open</p><p><em><strong>Share</strong></em> - share content</p><p><em><strong>Deeplink</strong></em> - an in-app link opening</p> |
| **`string ActionString`**                         | The property that returns an optional action identifier                                                                                                                                                                                                                                                                                                  |
| **`IDictionary<string,string> AdditionalData()`** | Additional data sent to push notification                                                                                                                                                                                                                                                                                                                |

### Class for handling buttons clicked in the notification (***`DTDActionButton`***)

| Property                       | Description                                                                                                                                                                                                                                                                                                                                              |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`DTDActionType ActionType`** | <p>The property that returns the value of enum’s <strong><code>DTDActionType</code></strong>.</p><p>Possible values:</p><p><em><strong>App</strong></em> - app open</p><p><em><strong>Url</strong></em> - external link open</p><p><em><strong>Share</strong></em> - share content</p><p><em><strong>Deeplink</strong></em> - an in-app link opening</p> |
| **`string ActionString`**      | Property that returns an optional action identifier                                                                                                                                                                                                                                                                                                      |
| **`string ButtonId`**          | Property that returns the ID of the clicked button                                                                                                                                                                                                                                                                                                       |
| **`string ButtonText`**        | Property that returns the text of the clicked button                                                                                                                                                                                                                                                                                                     |
| **`string ButtonIcon`**        | Property that returns the button icon name                                                                                                                                                                                                                                                                                                               |
| **`bool IsBackground`**        | <p>The button-click app open mode<br></p>                                                                                                                                                                                                                                                                                                                |


# iOS

iOS Integration

## Platform integration

#### Creating a Universal Push Notification Client SSL Certificate

You use Member Center to generate a push notification client SSL certificate that allows your notification server to connect to the APNs. Each App ID is required to have its own client SSL certificate. The client SSL certificate Member Center generates is a universal certificate that allows your app to connect to both the development and production environments.

{% hint style="info" %}
Only a team agent or admin can generate Apple Push Notification service SSL certificates.
{% endhint %}

To generate a universal client SSL certificate

1. In [Certificates, Identifiers & Profiles](http://developer.apple.com/account), select Certificates.
2. Click the Add button (+)<br>

   <figure><img src="/files/NKMEsJ3dUEKX0RDJJ1PE" alt=""><figcaption></figcaption></figure>
3. Under Production, select the “Apple Push Notification service SSL (Sandbox & Production)” checkbox, and click Continue.<br>

   <figure><img src="/files/DiKWIN1Yq8A9cqVn2JPO" alt=""><figcaption></figcaption></figure>
4. Choose an App ID from the App ID pop-up menu, and click Continue. Choose the explicit App ID that matches your bundle ID.
5. Create a certificate request on your Mac.
6. Click Choose File.
7. In the dialog that appears, select the certificate request file (with a .certSigningRequest extension), and click Choose File.
8. Click Generate.
9. Click Download.<br>

   <figure><img src="/files/Cl40qIyb8iMvhQIRfaKx" alt=""><figcaption></figcaption></figure>

#### **Follow these steps to export the certificate from Apple web-site to the P12-file:**

1. Open "Keychain access" application
2. If the certificate hasn't been added to keychain access yet, choose "File" →  "Import". Find the certificate file (CER-file) provided by Apple
3. Choose "Keys" section in "Keychain access" application
4. Choose a personal key associated with your iPhone developer certificate. Personal key is identified by open certificate associated with it "iPhone developer: ". Choose "File" → Export objects. Save key as .p12
5. You'll be suggested to create a password which is used when you need to import the key to another computer

#### Upload the certificate to the site

Upload the .p12-file into Integration section of application settings panel  (Settings -> Push Notifications):

![](/files/fUHlyrKAPgkp4UD25veB)

## Module initialization

1\. For the Messaging module to function you need the basic Analytics package. Before the notification initialization, you need to initialize the SDK. More about it you can read here: [Unity Integration](/integration/integration-of-sdk-v2/sdk-integration/unity).

2\. After the DTDAnalytics initialization block call the `StartNotificationService` method to activate the Messaging module:

```csharp
DTDMessaging.IOS.StartNotificationService();
```

#### Optional:

You can listen to basic notification module events. To do this, create a class that implements the **`IDTDPushListener`** interface and pass it to the **`DTDMessaging.IOS.SetPushListener`** method.

Example:

```csharp
public class MyPushListener : IDTDPushListener
{
    public void OnPushServiceRegistrationSuccessful(string deviceId)
    {
        Debug.Log(deviceId);
    }

    public void OnPushServiceRegistrationFailed(string error)
    {
        Debug.Log(error);

    }

    public void OnPushNotificationReceived(DTDPushMessage message)
    {
        Debug.Log(message);
    }

    public void OnInvisibleNotificationReceived(DTDPushMessage message)
    {
        Debug.Log(message);
    }

    public void OnPushNotificationOpened(DTDPushMessage pushMessage, DTDActionButton actionButton)
    {
        Debug.Log(pushMessage.ToString());
        Debug.Log(actionButton.ToString());
    }
}
```

You can specify the necessary display options using the **`DTDMessaging.IOS.SetNotificationOptions`** method.

Example:

```csharp
DTDMessaging.IOS.SetNotificationOptions(DTDNotificationOptions.Alert | DTDNotificationOptions.Badge | DTDNotificationOptions.Sound);
```

A complete example of the notification module initialization:

```csharp
public class MyPushListener : IDTDPushListener
{
    public void OnPushServiceRegistrationSuccessful(string deviceId)
    {
        Debug.Log(deviceId);
    }

    public void OnPushServiceRegistrationFailed(string error)
    {
        Debug.Log(error);

    }

    public void OnPushNotificationReceived(DTDPushMessage message)
    {
        Debug.Log(message);
    }

    public void OnInvisibleNotificationReceived(DTDPushMessage message)
    {
        Debug.Log(message);
    }

    public void OnPushNotificationOpened(DTDPushMessage pushMessage, DTDActionButton actionButton)
    {
        Debug.Log(pushMessage.ToString());
        Debug.Log(actionButton.ToString());
    }
}

public class NotificationExample : MonoBehaviour
{
    private const string APP_KEY = "***************"
    void Start()
    {
        DTDAnalytics.Initialize(APP_KEY);
        DTDMessaging.IOS.SetNotificationOptions(DTDNotificationOptions.Alert | DTDNotificationOptions.Badge | DTDNotificationOptions.Sound);
        DTDMessaging.IOS.SetPushListener(new PushListener());
        DTDMessaging.IOS.StartNotificationService();
    }
}
```

### External interface of the DTDMessaging module for iOS platform

| Methodes                                                              | Description                                                                                                                                                                                                                                                                                                                                                                                        |
| --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`void DTDMessaging.IOS.StartPushService()`**                        | <p>The method responsible for push notification activation:</p><ul><li>Requests user permission to receive push notifications</li><li>Sends push token and current state <strong><code>isAllowed</code></strong></li></ul>                                                                                                                                                                         |
| **void DTDMessaging.IOS.GetToken(Action\<string> onGetToken)**        | The method that returns current push token to **`onGetToken`** callback                                                                                                                                                                                                                                                                                                                            |
| **`DTDMessaging.IOS.SetPushListener(IDTDPushListener listener)`**     | The method for assigning a push notification event listener                                                                                                                                                                                                                                                                                                                                        |
| **`void DTDMessaging.IOS.PushNotificationsOptions(uint options)`**    | <p><em><strong>options</strong></em> is responsible for setting up the display of Push Notifications. It is set by the developer to select the method of notifying the user. It can be changed by the end-user.<br></p><p> By default, it has the value:</p><p><em><strong><code>\[.DTDNotificationOptionBadge, .DTDNotificationOptionSound, .DTDNotificationOptionAlert]</code></strong></em></p> |
| **`void DTDMessaging.IOS.PushIsAllowed (bool isAllowed )`**           | The method is responsible for enabling / disabling the ability to send a push notification to the user from devtodev.                                                                                                                                                                                                                                                                              |
| **`void DTDMessaging.IOS.GetPushState(Action<bool> onGetPushState)`** | The method that returns the push module state to **`onGetPushState`** callback                                                                                                                                                                                                                                                                                                                     |

### IDTDPushListener interface

| Delegate method                                                                                 | Description                                                                                                                                                           |
| ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`void OnPushServiceRegistrationSuccessful (string deviceId);`**                               | The method is called if a push token is successfully received, represented by a string.                                                                               |
| **`void OnPushServiceRegistrationFailed (string error);`**                                      | The method is called if at the time of receiving a pushToken errors occur. It passes the text of the error that occurred                                              |
| **`void OnInvisibleNotificationReceived (DTDPushMessage message);`**                            | The method is called when an invisible remote push notification is received. The **`DTDPushMessage`** object is passed                                                |
| **`void OnPushNotificationReceived (DTDPushMessage message);`**                                 | The method is called when a remote push notification is received while the application runs in the ***Foreground state***. The **`DTDPushMessage`** object is passed. |
| **`void OnPushNotificationOpened (DTDPushMessage pushMessage, DTDActionButton actionButton);`** | The method is called when the end-user opens a remote push notification. The **`DTDPushMessage`** object and the optional **`DTDActionButton`** object are passed.    |

### Class for receiving Notification data (DTDPushMessage). Main class properties

| Property                                          | Description                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`IDictionary<string,string> GetData():`**       | Complete information passed with the push notification.                                                                                                                                                                                                                                                                                                                                                                            |
| **`DTDActionType ActionType:`**                   | <p>The property that returns the value of enum’s <code>DTDActionType</code>.</p><p>Possible values:</p><p><em><strong><code>App</code></strong></em> - app open</p><p><em><strong><code>Url</code></strong></em> - external link open</p><p><em><strong><code>Share</code></strong></em> - share content</p><p><em><strong><code>Deeplink</code></strong></em> - open a link that leads straight to a specific in-app location</p> |
| **`string ActionString`**                         | The property that returns an optional action identifier                                                                                                                                                                                                                                                                                                                                                                            |
| **`IDictionary<string,string> AdditionalData()`** | Additional data sent to push notification                                                                                                                                                                                                                                                                                                                                                                                          |

### Class for handling buttons clicked in the notification (DTDActionButton)

| Property                       | Description                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`DTDActionType ActionType`** | <p>The property that returns the value of enum’s <strong><code>DTDActionType</code></strong>.</p><p>Possible values:</p><p><em><strong><code>App</code></strong></em> - app open</p><p><em><strong><code>Url</code></strong></em> - external link open</p><p><em><strong><code>Share</code></strong></em> - share content</p><p><em><strong><code>Deeplink</code></strong></em> - open a link that leads straight to a specific in-app location</p> |
| **`string ActionString`**      | The property that returns an optional action identifier                                                                                                                                                                                                                                                                                                                                                                                             |
| **`string ButtonId`**          | The property that returns the ID of the clicked button                                                                                                                                                                                                                                                                                                                                                                                              |
| **`string ButtonText`**        | The property that returns the text of the clicked button                                                                                                                                                                                                                                                                                                                                                                                            |

### DTDNotificationOptions

It is an OptionSet used for push notification authorization and configuration interaction with users.

**Attention**: The user can change the allowed parameters in the notification settings at any time.

Possible values:

* ***`DTDNotificationOptionBadge`*** - an option for displaying a badge on the application icon
* ***`DTDNotificationOptionSound`*** - an option for playing sound
* ***`DTDNotificationOptionAlert`*** - an option for displaying an alert
* ***`DTDNotificationOptionCarPlay`*** - an option for showing a push notification in CarPlay  ***`DTDNotificationOptionCriticalAlert`*** - an option for playing sound for critical alerts regardless of whether **“do not disturb”** is on or not. (Critical alerts require special permission from Apple). **Available from iOS 12.0 onwards**
* ***`DTDNotificationOptionProvidesSettings`*** - an option for indicating that the system should show a notification settings button in the application. **Available from iOS 12.0 onwards**
* ***`DTDNotificationOptionProvisional`*** - an option for sending provisional notifications to the Notification Center without interrupting functioning processes. **Available from iOS 12.0 onwards**
* ***`DTDNotificationOptionAnnunciation`*** - an option for Siri to automatically read messages through AirPods. **Available from iOS 13.0 onwards**

Attention: ***`DTDNotificationOptionProvisional`*** - Provisional push notifications are shown in the User Notification Center but not on the lock screen. This type of push notification doesn’t require an explicit opt-in from the user. You can start sending them as soon as the user installs and launches your app. However, the user can also explicitly enable/disable your notifications.

Use this setting to avoid the permission request on app launch because it is seen as intrusive, and most users opt-out of it.

It’s also important that when using the ***`DTDNotificationOptionProvisional`*** setting, the user needs to go to Notification Center settings to allow explicit notifications.

### **XCODE build**

After the build, open the **signing and capabilities** tab in the XCode project settings and add "**Push Notifications**" and "**Background Modes**" (tick the box against Remote notifications).

### Notification with Attachments

The framework provides support for iOS 10+ notification attachments, such as images, animated gifs, and video. In order to take advantage of this functionality, you will need to create a notification service extension alongside your main application. Create a new iOS target in Xcode (File -> New -> Target) and select the Notification Service Extension type. ​In the Member Center, a Push Notifications service will appear as Configurable (not Enabled) until you create a client SSL certificate.

The framework supports notification attachments: images, audio, and video. To enable the functionality, you need to complete several steps:

* Create a new iOS target in Xcode (File -> New -> Target) and select the Notification Service Extension&#x20;

![](/files/BiJp3tByS1qpzxUW7SuD)

* Set language to ‘Swift’

![](https://lh3.googleusercontent.com/L8tStcL4AHXyYzRMaZdtuyvnGvwjPPeLJWMmY900GvpPADbh0PvU5IQjFMQBnTfeTuQLGoXdnr8F4fc0lSH2iRuVRKZO_2WrW9oR-5a9J5esf1ovELTaUPSmPjSsx3cE3o96BpWoyaDnzsuulA)

* In the next window, click ‘Activate’

![](https://lh4.googleusercontent.com/0z2ZK052LiU56kztwCnfuWcZVHoxKAa7qPMCDAjUvLHNcdR1FViUxJNPiIVjMFubEefUp6POTiItwn10fXnMxTUey6pwmUFKV7iDXYuPPpMA_AkTnT5hmjI1VpKTEZ1jX_iZl_Ms9826UbDEhA)

* In the ‘Build Settings’ tap, change 'Architectures’ to ‘Standard Architecture (arm64, armv7)

![](https://lh5.googleusercontent.com/yLzbmj5E0q4lMtzmsIiavT_GrjCc5mJH9N5Qfo0Gd4ddEFZAbCVW8GdGNc_E6lYjTi1Cvfr5ywQrMuOydTQghekiVTMB-iexOpa-QxPl0zEME9JrGE3yCmDFr3RVRhPWvc4NuQt-KAl6A_t-cQ)

* Open the ‘Frameworks’ folder in your project, find the ‘DTDMessagingUnity’ framework and tick the box of your target in the ‘Target Membership’ section

![](https://lh3.googleusercontent.com/_0Dj9dp-QAeRF6ptzu8IiGORE1JaO3JEPNmZO0AGvSXSxtWDeysFcQRUidfZGMiyCOJmgsND3XcohbW6Tq8vQrcVa6yIOgHRh2c04cboD0Mgg6r1U1WhcoZ6D8OZh-R_6l0ehscuZp07-mFFyw)

* In the NotificationService class (in the folder named after your target), replace the code with the following:

```
import UserNotifications
import DTDMessagingUnity

class NotificationService: DTDMediaAttachmentExtension {

}
```

### Custom sounds

To use custom sounds for push notifications, you need to copy the necessary sound files to the Libraries folder of your Xcode project. You can read more about supported formats [here](https://developer.apple.com/documentation/usernotifications/unnotificationsound#2943048).

<figure><img src="/files/Q5MAAc0JxxuDKDPODBxU" alt=""><figcaption></figcaption></figure>

When creating a push company, it is important to specify the full name of the sound with file extension (for example, sound.wav).


# Windows (UWP/WSA)

WSA Integration

## Module initialization

1. For the Messaging module to function you need the basic Analytics package. Before the notification initialization, you need to initialize the SDK. More about it you can read here: [Unity Integration](/integration/integration-of-sdk-v2/sdk-integration/unity).
2. Add the **`DTDAnalytics`** initialization block after the **`DTDMessaging`** initialization block.

```csharp
#if UNITY_WSA
  DTDMessaging.WSA.SetMessagingEnabling(true);
#endif#
```

{% hint style="info" %}
Attention! Use the `#if UNITY_WSA` define to surround any notification module code on the WSA platform.
{% endhint %}

```csharp
#if UNITY_WSA
  DTDMessaging.WSA.{AnyMethod}
#endif#
```

### Optional

#### Module status check

Use the following method to check current status:

```csharp
void GetMessagingEnabling(Action<bool> onGetMessagingEnabling)
```

The current module status will be sent to **`onGetMessagingEnabling`** callback.

#### Listening to events

You can listen to basic events of the Messaging module: create the class that implements the **`IDTDPushListener`** interface and send it to the **`DTDMessaging.WSA.SetPushListener`** method.

Class example:

```csharp
public class MyPushListener : IDTDPushListener
{
    public void OnPushServiceRegistrationSuccessful(string deviceId)
    {
        Debug.Log(deviceId);
    }

    public void OnPushServiceRegistrationFailed(string error)
    {
        Debug.Log(error);

    }

    public void OnPushNotificationReceived(DTDPushMessage message)
    {
        Debug.Log(message);
    }

    public void OnInvisibleNotificationReceived(DTDPushMessage message)
    {
        //IOS only.
    }

    public void OnPushNotificationOpened(DTDPushMessage pushMessage, DTDActionButton actionButton)
    {
        Debug.Log(pushMessage.ToString());
        Debug.Log(actionButton.ToString());
    }
}
```

A complete example of notification module initialization:

```csharp
public class MyPushListener : IDTDPushListener
{
    public void OnPushServiceRegistrationSuccessful(string deviceId)
    {
        Debug.Log(deviceId);
    }

    public void OnPushServiceRegistrationFailed(string error)
    {
        Debug.Log(error);
    }

    public void OnPushNotificationReceived(DTDPushMessage message)
    {
        Debug.Log(message);
    }

    public void OnInvisibleNotificationReceived(DTDPushMessage message)
    {
        Debug.Log(message);
    }

    public void OnPushNotificationOpened(DTDPushMessage pushMessage, DTDActionButton actionButton)
    {
        Debug.Log(pushMessage.ToString());
        Debug.Log(actionButton.ToString());
    }
}

public class NotificationExample : MonoBehaviour
{
    private const string APP_KEY = "***************"
    void Start()
    {
        DTDAnalytics.Initialize(APP_KEY);
#if UNITY_WSA
        DTDMessaging.WSA.SetPushListener(new PushListener());
        DTDMessaging.WSA.SetMessagingEnabling(true);
#endif
    }
}
```

### External interface of the DTDMessaging module

| Method                                                                                | Description                                                                                              |
| ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| **`void DTDMessaging.WSA.SetMessagingEnabling(bool value)`**                          | The method responsible for enabling or disabling of the push notification module                         |
| **`void DTDMessaging.WSA.GetMessagingEnabling(Action<bool> onGetMessagingEnabling)`** | The method that returns current state of the push notification module to onGetMessagingEnabling callback |
| **`void DTDMessaging.WSA.SetPushListener (IDTDPushListener pushListener)`**           | It sets a listener for push notification event trapping                                                  |

### Build a Windows Store App in Unity <a href="#build-a-windows-store-app-in-unity" id="build-a-windows-store-app-in-unity"></a>

Build a Windows Store App in Unity. After the app is built, a Visual Studio project will be created. Proceed with the following changes.

There is a difference in the implementation of the elements mentioned below for **different types of projects**:

#### IL2CPP + XAML

Put the following source in your App class (usually it is ***`App.xaml.cpp`*** file). Add several lines of code in a generated **`App.xaml.cpp`** class. After defining headers:

```csharp
//...headers

extern "C" __declspec(dllimport) void __stdcall AddActivatedEventArgs(IInspectable* activatedEventArgs);
```

And at the end of of the **`App::OnLaunched(LaunchActivatedEventArgs^ e)`** and **`App::OnActivated(IActivatedEventArgs^ args)`** functions.

For Example:

```csharp
void App::OnActivated(IActivatedEventArgs^ args) 
{
 //...other code
 AddActivatedEventArgs(reinterpret_cast<IInspectable*>(static_cast<Platform::Object^>(args)));
}

void App::OnLaunched(LaunchActivatedEventArgs^ e) 
{
 auto args = static_cast<IActivatedEventArgs^>(e);
 AddActivatedEventArgs(reinterpret_cast<IInspectable*>(static_cast<Platform::Object^>(args)));
}
```

#### **IL2CPP + D3D**

Put the following source in your App class (usually it is App.cpp file). Add several lines of code in a generated **App.cpp** class. After defining headers:

```csharp
//...headers
extern "C" __declspec(dllimport) void __stdcall AddActivatedEventArgs(IInspectable* activatedEventArgs);
```

```csharp
void App::OnActivated(CoreApplicationView^ sender, IActivatedEventArgs^ args) 
{
 //...other code
 AddActivatedEventArgs(reinterpret_cast<IInspectable*>(static_cast<Platform::Object^>(args)));
}
```

And at the end of of the **`App::OnActivated(CoreApplicationView^ sender, IActivatedEventArgs^ args`)** function.

Besides, in the UI editor of the ***`Package.appxmanifest`*** file you need to do the following:

1. Add Background Tasks in the Declarations tab and mark it as ***`System Event`***. After that, add **`DevToDev.Background.ToastNotificationBackgroundTask`** to the ***`Entry Point field`***.
2. Add Background Tasks in the Declarations tab and mark it as ***`Push Notification`***. After that, add **`DevToDev.Background.RawNotificationBackgroundTask`** to the ***`Entry Point field`***.

### Disabling

To disable notifications, call the following method:

```csharp
DTDMessaging.WSA.SetMessagingEnabling(false);
```


# Unreal Engine

## Plugin installation

The SDK can be found in the devtodev [GitHub repository](https://github.com/devtodev-analytics/unreal-sdk-2.0). Download the latest release of the [Source code (zip)](https://github.com/devtodev-analytics/unreal-sdk-2.0/releases/latest). Unzip the archive and copy the ***`DTDMessaging`*** folder to the ***Plugins*** folder of your project.

If you have a C++ type project, add **`DTDMessaging`** to the list of dependency names in the ***`<module_name>.Build.cs`*** file of the module in which you plan to use the plugin.

Example:

```cpp
PublicDependencyModuleNames.Add("DTDMessaging");
```

### Android

Add your ***`google-services.json`*** file to the project's root directory. It will be used to configure notifications during the project-building process.

### iOS

***In the case your Unreal Engine is built from GitHub source code:***

Enable notifications in the settings of your project: *`Edit → Project Settings → iOS → Enable Remote Notifications Support`*

***In the case your Unreal Engine is not built from GitHub source code:***

Add the parameter to the engine configuration file (*`<proj_dir>/Config/DefaultEngine.ini`*):

```cpp
// Some code/Script/IOSRuntimeSettings.IOSRuntimeSettings]
bEnableRemoteNotificationsSupport=True
```

## Data types

#### **`class UDTDMessagingBPLibrary`**

A class that implements analytic methods.

Header file:

```cpp
#include "DTDMessaging/Public/DTDMessagingBPLibrary.h"
```

#### **`enum class EDTDNotificationActionType : uint8`**

Notification action type

Header file:

```cpp
#include "DTDMessaging/Public/DTDNotificationActionType.h"
```

Values:

* ***`App = 0`*** - default value
* ***`Url = 1`*** - external link opening
* ***`Share = 2`*** - share contentc
* ***`DeepLink = 2`*** - an in-app link opening

#### **`struct FDTDNotification`**

Notification data container.

Header file:

```cpp
#include "DTDMessaging/Public/DTDNotification.h"
```

| Member             | Type                       | Description                        |
| ------------------ | -------------------------- | ---------------------------------- |
| **`ActionType`**   | EDTDNotificationActionType | Тип действия уведомления           |
| **`ActionString`** | FString                    | Идентификатор действия уведомления |
| **`Data`**         | TMap\<FString, FString>    | <p>Данные уведомления<br></p>      |

#### **`struct FDTDNotificationAction`**

Notification action data container.

Header file:

```cpp
#include "DTDMessaging/Public/DTDNotificationAction.h
```

| Member             | Type                       | Description                        |
| ------------------ | -------------------------- | ---------------------------------- |
| **`ActionType`**   | EDTDNotificationActionType | Тип действия уведомления           |
| **`ActionString`** | FString                    | Идентификатор действия уведомления |
| **`ButtonId`**     | FString                    | Идентификатор нажатой кнопки       |
| **`ButtonText`**   | FString                    | Текст нажатой кнопки               |
| **`ButtonIcon`**   | FString                    | Иконка нажатой кнопки              |
| **`IsBackground`** | bool                       | Режим открытия приложения кнопкой  |

#### **`enum class EDTDIOSNotificationOptions : uint8`**

{% hint style="warning" %}
iOS only
{% endhint %}

Push Notification display settings are **`Bitflags`** that are managed by the developer and allow for selecting the method of user notification. It can be altered by the end user.

By default, it has the value: **`Badge|Sound|Alert`**

Header file:

```cpp
#include "DTDMessaging/Public/DTDIOSNotificationOptions.h"
```

Values:

* ***`None = 0`*** - nothing
* ***`Badge = 1 << 0`*** - can display a badge on the app icon
* ***`Sound = 1 << 1`*** - can play a sound
* ***`Alert = 1 << 2`*** - can display an alert
* ***`CarPlay = 1 << 3`*** - can display a push notification on CarPlay
* ***`CriticalAlert = 1 << 4`*** - critical alerts can play a sound even if **Do Not Disturb** is enabled (critical alerts require a special entitlement issued by Apple). **Available from iOS 12 onwards.**
* ***`AppNotificationSettings = 1 << 5`*** - this option defines that the system should display a notification settings button in the app. **Available from iOS 12.0 onwards.**
* ***`Provisional = 1 << 6`*** - an option for sending provisional notifications to the Notification Center without interrupting functioning processes. **Available from iOS 12.0 onwards.**

{% hint style="info" %}
*Provisional* - Provisional push notifications are shown in the User Notification Center but not on the lock screen. This type of push notification doesn’t require an explicit opt-in from the user. You can start sending them as soon as the user installs and launches your app. However, the user can also explicitly enable/disable your notifications.
{% endhint %}

Use this setting to avoid the permission request on app launch because it is seen as intrusive, and most users opt out of it.

It’s also important that when using the Provisional setting, the user needs to go to Notification Center settings to allow explicit notifications.

#### **`Delegates`**

Header file:

```cpp
#include "DTDMessaging/Public/DTDMessagingDelegates.h"
```

Delegates:

```cpp
DECLARE_DELEGATE_OneParam(FDTDMessagingBoolParamsDelegate, bool);
DECLARE_DELEGATE_OneParam(FDTDMessagingStringParamsDelegate, const FString&);
DECLARE_DELEGATE_OneParam(FDTDMessagingNotificationParamsDelegate, const FDTDNotification&);
DECLARE_DELEGATE_TwoParams(FDTDMessagingNotificationActionParamsDelegate, const FDTDNotification&, const FDTDNotificationAction&);
```

## Initialization

Notification module initialization:

![](/files/kCZSCTml8Ba0GMNGrtAx)

```cpp
UDTDMessagingBPLibrary::Initialize();
```

## Methods

#### **`SetAvailability`**

A method responsible for enabling/disabling push notifications. When the state changes, it sends an event that includes the **availability** status (**true** or **false**). The **availability** status flag is stored in the SDK.

![](/files/rZJfP1rWmMIfQ3M7GbbS)

```cpp
UDTDMessagincgBPLibrary::SetAvailability(true);
```

#### **`GetAvailability`**

Get the current **`availability`** status flag (**true** or **false**):

![](/files/OaBFTSINwYFHgOekgPnv)

| Argument       | Type                                                                                             | Description |
| -------------- | ------------------------------------------------------------------------------------------------ | ----------- |
| **`onResult`** | <ul><li>FDTDMessagingDynamicBoolParamsDelegate</li><li>FDTDMessagingBoolParamsDelegate</li></ul> | Callback    |

```cpp
auto onResult = new FDTDMessagingBoolParamsDelegate();
onResult->BindLambda([](bool value)
{
  // Your code...
});
UDTDMessagingBPLibrary::GetAvailability(*onResult);
```

#### **`GetToken`**

Get a current unique device ID used in the notification system:

![](/files/nGAN6vUhnsxoF2G8gwYB)

| Argument       | Type                                                                                                 | Description |
| -------------- | ---------------------------------------------------------------------------------------------------- | ----------- |
| **`onResult`** | <ul><li>FDTDMessagingDynamicStringParamsDelegate</li><li>FDTDMessagingStringParamsDelegate</li></ul> | Callback.   |

```cpp
auto onResult = new FDTDMessagingStringParamsDelegate();
onResult->BindLambda([](const FString& value)
{
  // Your code...
});
UDTDMessagingBPLibrary::GetToken(*onResult);
```

#### **`SetTokenListener`**

Set a token listener. The listener will be executed when the SDK updates a unique device ID in the notification system.

![](/files/nKk362ubNlRoa0JGzBKI)

| Argument       | Type                                                                                                 | Description |
| -------------- | ---------------------------------------------------------------------------------------------------- | ----------- |
| **`listener`** | <ul><li>FDTDMessagingDynamicStringParamsDelegate</li><li>FDTDMessagingStringParamsDelegate</li></ul> | Listener.   |

```cpp
auto listener = new FDTDMessagingStringParamsDelegate();
listener->BindLambda([](const FString& value)
{
  // Your code...
});
UDTDMessagingBPLibrary::SetTokenListener(*listener);
```

#### **`SetTokenErrorListener`**

Set a listener for errors in token reception. The listener will be executed when an error occurs while the SDK updates a unique device ID in the notification system.

![](/files/PgtjmTDZPjKuyanhxSWz)

| Argument       | Type                                                                                                 | Description |
| -------------- | ---------------------------------------------------------------------------------------------------- | ----------- |
| **`listener`** | <ul><li>FDTDMessagingDynamicStringParamsDelegate</li><li>FDTDMessagingStringParamsDelegate</li></ul> | Listener.   |

```cpp
auto listener = new FDTDMessagingStringParamsDelegate();
listener->BindLambda([](const FString& value)
{
  // Your code...
});
UDTDMessagingBPLibrary::SetTokenErrorListener(*listener);
```

#### **`SetNotificationReceiveListener`**

Set a listener for notification reception. The listener will be executed when the SDK receives a notification.

<img src="/files/IAV37BTq8HHN3DKK28FM" alt="" data-size="original">

| Argument       | Type                                                                                                             | Description |
| -------------- | ---------------------------------------------------------------------------------------------------------------- | ----------- |
| **`listener`** | <ul><li>FDTDMessagingDynamicNotificationParamsDelegate</li><li>FDTDMessagingNotificationParamsDelegate</li></ul> | Listener.   |

```cpp
auto listener = new FDTDMessagingNotificationParamsDelegate();
listener->BindLambda([](const FDTDNotification& notification)
{
  // Your code...
});
UDTDMessagingBPLibrary::SetNotificationReceiveListener(*listener);
```

#### **`SetInvisibleNotificationReceiveListener`**

Set a listener for invisible notification reception. The listener will be executed when the SDK receives an invisible notification.

![](/files/wONSenzCPbhm59ogSDCG)

| Argument       | Type                                                                                                             | Description |
| -------------- | ---------------------------------------------------------------------------------------------------------------- | ----------- |
| **`listener`** | <ul><li>FDTDMessagingDynamicNotificationParamsDelegate</li><li>FDTDMessagingNotificationParamsDelegate</li></ul> | Listener.   |

```cpp
auto listener = new FDTDMessagingNotificationParamsDelegate();
listener->BindLambda([](const FDTDNotification& notification)
{
  // Your code...
});
UDTDMessagingBPLibrary::SetInvisibleNotificationReceiveListener(*listener);
```

#### **`SetNotificationActionListener`**

Set a listener for notification activation. The listener will be executed when the notification gets activated.

<img src="/files/q5jnwsEWytp7ACfhmZQU" alt="" data-size="original">

| Argument       | Type                                                                                                                         | Description |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------- |
| **`listener`** | <ul><li>FDTDMessagingDynamicNotificationActionParamsDelegate</li><li>FDTDMessagingNotificationActionParamsDelegate</li></ul> | Listener.   |

```cpp
auto listener = new FDTDMessagingNotificationActionParamsDelegate();
listener->BindLambda([](const FDTDNotification& notification, const FDTDNotificationAction& action)
{
  // Your code...
});
UDTDMessagingBPLibrary::SetNotificationActionListener(*listener);
```

#### **`IOSSetNotificationOptions`**

{% hint style="warning" %}
iOS only
{% endhint %}

Set notification parameters.

<img src="/files/lugq54mXO2PoHS1SrIgl" alt="" data-size="original">

| Argument      | Type  | Description |
| ------------- | ----- | ----------- |
| **`options`** | int32 | Options.    |

{% hint style="warning" %}
An **int32** type value is used as an argument of this method. However, this argument should be calculated by using enumerators of **`EDTDIOSNotificationOptions`** and bitwise OR operator.
{% endhint %}

```cpp
int32 options = EDTDIOSNotificationOptions::Badge |
  EDTDIOSNotificationOptions::Sound |
  EDTDIOSNotificationOptions::Alert;
UDTDMessagingBPLibrary::IOSSetNotificationOptions(options);
```


# A/B testing


# Description of A/B testing on the SDK side

{% hint style="danger" %}
**This integration manual is only for SDK versions below 2.6.0 / Unity 3.10.0.**&#x20;

If you are using SDK 2.6.0 / Unity 3.10.0 and higher, please refer to the [updated integration manual](/integration/integration-of-sdk-v2/remote-configuration/rc-integration).
{% endhint %}

## Step 1&#x20;

Some operations can take a while because the SDK operates asynchronously. These operations are:&#x20;

### **A/B test configuration.**

After you’ve created an A/B test in the web interface, the SDK will receive its configuration via the network. Use the  **`remoteConfigWaiting`** property in the **`DTDRemoteConfig`** class to set the maximum time allowed for waiting for the A/B test configuration.

{% hint style="info" %}
`remoteConfigWaiting`is set in seconds
{% endhint %}

By default, the **`remoteConfigWaiting`** value is equal to null. This means that the test configuration waiting time is unlimited. The limitation is important in case the test that you are about to execute can influence your app functioning.

Example: *`DTDRemoteConfig.remoteConfigWaiting = 10`*&#x20;

In this case, the SDK will wait for the A/B test configuration for 10 seconds. If it won’t receive the config during the allocated time, it will notify the developer by using the `onReceived(result: DTDRemoteConfigReceiveResult)` method with **Failure** value and disable the A/B testing until the next SDK initialization.

### Waiting for an A/B test group.

After the SDK finds a test, it will wait for a suitable group to include the user into and start the experiment.

{% hint style="info" %}
By default, waiting time (`groupDefinitionWaiting`) is 10 seconds.
{% endhint %}

You can change this value up or down.

Example: `DTDRemoteConfig.groupDefinitionWaiting = 15`

In this case, the SDK will wait for a group for no more than 15 seconds. If It can't find a suitable test for 15 seconds, it will be cancelled and its activation will be impossible. After the next SDK initialization (app restart), the user will have another chance to participate in the test.

{% hint style="warning" %}
The `DTDRemoteConfig.remoteConfigWaiting` and `DTDRemoteConfig.groupDefinitionWaiting`values can be set only prior to SDK initialization!
{% endhint %}

## Step 2

In the `DTDRemoteConfig` class you need to set the default variables and their values using the `DTDRemoteConfig.defaults` property.

{% hint style="info" %}
The variable values in the `defaults` property do not change on the SDK side.
{% endhint %}

After setting up `DTDRemoteConfig.defaults`, you can receive the variable values by using the `DTDRemoteConfig.config` property.

{% hint style="warning" %}
When working with A/B tests, always use the `config` property to receive the actual variables and their values!
{% endhint %}

{% hint style="info" %}
If for some reasons, the device can not be included in the test (no network or network problems, devtodev did not assign it to a group, incorrect SDK implementation, etc.)  `DTDRemoteConfig.config` will return default values. Using this approach, you can always shape the desirable UI and the business logic of the app.
{% endhint %}

## **Step 3**

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}
To work with A/B testing, use the `DTDAnalytics.initializeWithAbTest` initialization method.

{% hint style="info" %}
If you used `DTDAnalytics.initialize` previously, you need to replace it with `DTDAnalytics.initializeWithAbTest` as the SDK initialization method.&#x20;
{% endhint %}

The `DTDRemoteConfigListener` interface is added to the  `initializeWithAbTest` method. It implements three methods:

* `onReceived(result: DTDRemoteConfigReceiveResult)` &#x20;
* `onPrepareToChange()`&#x20;
* `onChanged(result: DTDRemoteConfigChangeResult, error: Error?)`

{% hint style="warning" %}
The `DTDRemoteConfigListener`methods are not called in the main thread - you need to keep this in mind when interacting with UI elements of the app. Also, do not execute operations that block the thread in the `DTDRemoteConfigListener` methods, because this will disable the SDK operation (see examples).
{% endhint %}
{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}
To work with A/B testing, use the `[DTDAnalytics applicationKey:* abConfigListener:*]` initialization method.

{% hint style="info" %}
If you used `[DTDAnalytics applicationKey:*]` previously, you need to replace it with \[DTDAnalytics applicationKey:\* abConfigListener:\*] as the SDK initialization method.&#x20;
{% endhint %}

The `DTDRemoteConfigListener` interface is added to the  `[DTDAnalytics applicationKey:* abConfigListener:*]` method. It implements three methods:

* `-(void)onReceivedResult:(enum DTDRemoteConfigReceiveResult)result`
* `-(void)onPrepareToChange`
* `-(void)onChangedResult:(enum DTDRemoteConfigChangeResult)result error:(NSError *)error`

{% hint style="warning" %}
The `DTDRemoteConfigListener`methods are not called in the main thread - you need to keep this in mind when interacting with UI elements of the app. Also, do not execute operations that block the thread in the `DTDRemoteConfigListener` methods, because this will disable the SDK operation (see examples).
{% endhint %}
{% endtab %}

{% tab title="Android (Kotlin)" %}
To work with A/B testing, use the `DTDAnalytics.initializeWithAbTest` initialization method.

{% hint style="info" %}
If you used `DTDAnalytics.initialize` previously, you need to replace it with `DTDAnalytics.initializeWithAbTest` as the SDK initialization method.&#x20;
{% endhint %}

The `IRemoteConfigListener` interface is added to the  `initializeWithAbTest` method. It implements three methods:

* `onReceived(result: DTDRemoteConfigReceiveResult)`&#x20;
* `onPrepareToChange()`&#x20;
* `onChanged(result: DTDRemoteConfigChangeResult, ex: Exception?)`

{% hint style="warning" %}
The `DTDRemoteConfigListener`methods are not called in the main thread - you need to keep this in mind when interacting with UI elements of the app. Also, do not execute operations that block the thread in the `DTDRemoteConfigListener` methods, because this will disable the SDK operation (see examples).
{% endhint %}
{% endtab %}

{% tab title=" Android (Java)" %}
To work with A/B testing, use the `DTDAnalytics.INSTANCE.initializeWithAbTest` initialization method.

{% hint style="info" %}
If you used `DTDAnalytics.INSTANCE.initialize` previously, you need to replace it with `DTDAnalytics.INSTANCE.initializeWithAbTest` as the SDK initialization method.&#x20;
{% endhint %}

The `IRemoteConfigListener` interface is added to the  `initializeWithAbTest` method. It implements three methods:

* `void onReceived(@NonNull DTDRemoteConfigReceiveResult result)`&#x20;
* `void onPrepareToChange()`&#x20;
* `void onChanged(@NonNull DTDRemoteConfigChangeResult result, @Nullable Exception ex)`

{% hint style="warning" %}
The `DTDRemoteConfigListener`methods are not called in the main thread - you need to keep this in mind when interacting with UI elements of the app. Also, do not execute operations that block the thread in the `DTDRemoteConfigListener` methods, because this will disable the SDK operation (see examples).
{% endhint %}
{% endtab %}

{% tab title="Unity" %}
To work with A/B testing, use the `DTDAnalytics.InitializeWithAbTests` initialization method.

{% hint style="info" %}
If you used `DTDAnalytics.Initialize` previously, you need to replace it with `DTDAnalytics.InitializeWithAbTests` as the SDK initialization method.&#x20;
{% endhint %}

The `DTDRemoteConfigListener` interface is added to the  `InitializeWithAbTests` method. It implements three methods:

* `OnReceived(DTDRemoteConfigReceiveResult result)` &#x20;
* `OnPrepareToChange()`&#x20;
* `OnChanged(DTDRemoteConfigChangeResult result, string exceptionMessage = null)`

{% hint style="warning" %}
The `DTDRemoteConfigListener`methods are not called in the main thread - you need to keep this in mind when interacting with UI elements of the app. Also, do not execute operations that block the thread in the `DTDRemoteConfigListener` methods, because this will disable the SDK operation (see examples).
{% endhint %}
{% endtab %}

{% tab title=".NET + UWP" %}
To work with A/B testing, use the `DTDAnalytics.InitializeWithAbTests` initialization method.

{% hint style="info" %}
If you used `DTDAnalytics.Initialize` previously, you need to replace it with `DTDAnalytics.InitializeWithAbTests` as the SDK initialization method.&#x20;
{% endhint %}

The `IDTDRemoteConfigListener` interface is added to the  `InitializeWithAbTests` method. It implements three methods:

* `OnReceived(DTDRemoteConfigReceiveResult result)`
* `OnPrepareToChange()`
* `OnChanged(DTDRemoteConfigChangeResult result, string error)`

{% hint style="warning" %}
The `DTDRemoteConfigListener`methods are not called in the main thread - you need to keep this in mind when interacting with UI elements of the app. Also, do not execute operations that block the thread in the `DTDRemoteConfigListener` methods, because this will disable the SDK operation (see examples).
{% endhint %}
{% endtab %}

{% tab title="Web" %}
{% hint style="warning" %}
**Known Limitations: A/B Testing in Restricted Browser Environments**

When running the A/B testing module in an iframe, certain browsers with strict privacy settings (e.g., Safari, Firefox, Brave) may block access to storage mechanisms such as cookies or `localStorage`. This limitation prevents the proper functioning of A/B tests in these environments. SDK is checking the possibility of accessing storage and will turn off the A/B testing for such a user/device if that's the case.
{% endhint %}

To work with A/B testing, use the `devtodev.initializeWithAbTest` initialization method.

If you used `devtodev.initialize` previously, you need to replace it with `devtodev.initializeWithAbTest` as the SDK initialization method.

The `initializeWithAbTest` method as a third parameter takes an object with 3 `Callable` as arguments, to work with A/B tests it is necessary to implement:

* `onReceived(result: DTDRemoteConfigReceiveResult)`
* `onPrepareToChange()`
* `onChanged(result: DTDRemoteConfigChangeResult, error: String)`

```javascript
window.devtodev.initializeWithAbTest(
    appKey, 
    {
        userId: userId,
        logLevel: logLevel,
        trackingAvailability: true,
    },
    {
        onReceived: function(result) {
            console.log('onReceived callback', result)
        },
        onPrepareToChange: function() {
            console.log('onPrepareToChange callback')
        },
        onChanged: function(result, error) {
            console.log('onChanged callback', result, error)
        }
    }
)
```

{% endtab %}

{% tab title="Unreal" %}
To work with A/B testing, use the `UDTDAnalyticsBPLibrary::InitializeWithAbTest` initialization method.

<figure><img src="/files/Vj7PiTh9RyWK2QVkZXNR" alt="" width="310"><figcaption><p>Blueprint</p></figcaption></figure>

{% hint style="info" %}
If you used `UDTDAnalyticsBPLibrary::Initialize` previously, you need to replace it with `UDTDAnalyticsBPLibrary::InitializeWithAbTest` as the SDK initialization method.&#x20;
{% endhint %}

Implements three delegate methods:

* `onRemoteConfigReceive(EDTDRemoteConfigReceiveResult result)`
* `onRemoteConfigPrepareToChange()`&#x20;
* `onRemoteConfigChange(EDTDRemoteConfigChangeResult result, FString error)`

<figure><img src="/files/fBwR08MOHNgaNCU6rJjE" alt="" width="375"><figcaption><p>Blueprint</p></figcaption></figure>

```cpp
DECLARE_DELEGATE(FDTDRemoteConfigPrepareToChangeDelegate);
DECLARE_DELEGATE_OneParam(FDTDRemoteConfigReceiveResultDelegate, EDTDRemoteConfigReceiveResult);
DECLARE_DELEGATE_TwoParams(FDTDRemoteConfigChangeResultDelegate, EDTDRemoteConfigChangeResult, const FString&);
```

{% endtab %}

{% tab title="Godot" %}
To work with A/B testing, use the `DTDAnalytics.InitializeWithAbTest` initialization method.

{% hint style="info" %}
If you used `DTDAnalytics.initialize` previously, you need to replace it with `DTDAnalytics.initializeWithAbTest` as the SDK initialization method.&#x20;
{% endhint %}

The `InitializeWithAbTest` method takes 3 `Callable` as arguments, to work with A/B tests it is necessary to implement:

* `onRemoteConfigReceive(result: GDDTDRemoteConfigReceiveResult.ReceiveResult)`
* `onRemoteConfigPrepareToChange()`
* `onRemoteConfigChange(result: GDDTDRemoteConfigChangeResult.ChangeResult, error: String)`
  {% endtab %}
  {% endtabs %}

## **Step 4**

Methods implementation for working with A/B tests. When suitable conditions for the test occur, the following chain of events gets initiated:

* Calling `onReceived` method
* Calling  `onPrepareToChange` method
* Calling  `onChanged` method

### **OnReceived**

The `onReceived`  method is triggered every time when the SDK loads A/B test configuration. The `result` argument returns the following:

* ***Success*** if the configuration was loaded in the time provided. Time is set by the user in `DTDRemoteConfig.remoteConfigWaiting.`
* ***Failure*** if the config wasn’t received during the provided time defined by the user in `DTDRemoteConfig.remoteConfigWaiting`.
* **Empty** if the configuration was loaded in the time provided, but the list of experiments is empty. Time is set by the user in `DTDRemoteConfig.remoteConfigWaiting`.&#x20;

{% hint style="warning" %}
If the `DTDRemoteConfig.remoteConfigWaiting`’s default value is equal to null, the `OnReceived` method and `DTDRemoteConfigReceiveResult.Success` are called when the config is received. The time of the OnReceived call depends on the network speed.
{% endhint %}

### OnPrepareToChange

The `onPrepareToChange()` method is designed to notify the developer that the configuration of the A/B tests will be changed (the test is found or canceled, switch to a user who is not participating in the experiment, etc. (see Some features in the behavior of interfaces)).

{% hint style="info" %}
SDK asynchronous behavior causes a gap in time between the moment the suitable conditions for the test occur and its possible activation. You need to keep this in mind when changing the app’s UI and UX and pause the user’s interaction with the app until it comes into action:
{% endhint %}

* `onChanged` (see `onChanged` description) and a decision on test participation with a new config is made (see `applyConfig` description).
* A timer on the developer’s side. The timer defines the time allocated for new config waiting. After the time has run out, waiting for the config should be canceled.

See the implementation example (Scenario 1)

After calling onPrepareToChange, the SDK executes an asynchronous request for entering the test. That will result in calling onChanged.

{% hint style="info" %}
In case of disruption of the Internet connection or loss of focus, the test will be considered canceled until the next app launch.
{% endhint %}

### onChanged

The `onChanged` method is triggered in one of the following cases:

* The DevToDev server offers to enroll in a test. \
  &#x20;`onChanged()` with `DTDRemoteConfigChangeResult.success` is called
* The DevToDev server refuses to execute the test.\
  `onChanged()` with `DTDRemoteConfigChangeResult.failure` is called
* The device has no internet connection.\
  `onChanged()` with `DTDRemoteConfigChangeResult.failure` is called
* If the offer is received after the 30 second waiting time has expired\
  `onChanged()` with `DTDRemoteConfigChangeResult.failure` is called
* The test was previously activated (during the initialization)\
  `onChanged()` with `DTDRemoteConfigChangeResult.success` is called

If the `onChanged` method is triggered with `status == DTDRemoteConfigChangeResult.success` value, the possible options are:

* Accept the configuration by calling the `DTDRemoteConfig.applyConfig()` method\
  If the configuration is accepted, the default parameters and group parameters (issued by the server) will overlap. After that, the parameters of the new configuration will be available in `DTDRemoteConfig.config` (you can use them to change the behavior or the interface of the app).
* Refuse enrollment in the test `DTDRemoteConfig.resetConfig()`\
  If the test enrollment is refused or the time for decision making is up, the SDK will temporarily flag the test and it will become unavailable until the next initialization. After the next initialization, the test will be available for participation when its requirements are fulfilled.

In the cases where `status` == `DTDRemoteConfigChangeResult.failure`, the `DTDRemoteConfig.applyConfig()` method will be triggered and the default configuration will stay in place.

## Examples of SDK integration intended for working with A/B tests

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}
Set the default values `DTDRemoteConfig.defaults` = `["button_color": "green", "button_size": "8"]`

Variable names have to be the same as in the ‘Group settings’ section of the web interface (see ‘web’ above). When the SDK gets included in the A/B test, the devtodev server automatically assigns it to a group.

{% hint style="warning" %}
SDK will not use new variable values that are not set in DTDRemoteConfig.defaults
{% endhint %}

Example of the `DTDRemoteConfig.config`value variants:

<table><thead><tr><th width="172">Key/Value</th><th width="150">defaults</th><th width="150"> Control Group</th><th width="150"> Group A</th><th> Group B</th></tr></thead><tbody><tr><td>button_color</td><td>green</td><td>green</td><td>blue</td><td>gray</td></tr><tr><td>button_size</td><td>8</td><td>8</td><td>12</td><td>14</td></tr></tbody></table>

Example of SDK initialization that includes A/B testing:

```swift
import UIKit
import DTDAnalytics

@main
class AppDelegate: UIResponder, UIApplicationDelegate, DTDRemoteConfigListener {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Config timeout, optional
        DTDRemoteConfig.remoteConfigWaiting = 10.0
        // Group timeout, optional
        DTDRemoteConfig.groupDefinitionWaiting = 15.0
        // Implementation defaults params
        DTDRemoteConfig.defaults = ["button_color": "green",
                                    "button_size": "8"]
        // Initialize SDK with ab test
        DTDAnalytics.initializeWithAbTest(applicationKey: "App ID",
                                          abConfigListener: self)
        return true
    }

    // The method is called when the configuration has been received
    func onReceived(result: DTDRemoteConfigReceiveResult) {
        print("Configuration received, result \(result)")
    }

    // Called when experiment is found
    func onPrepareToChange() {
        print("Experiment found")
    }

    // Called when SDK receives config
    func onChanged(result: DTDRemoteConfigChangeResult, error: Error?) {
        print("DTDRemoteConfigChangeResult: \(result)")
        if let error = error {
            print("DTDRemoteConfigError: \(error.localizedDescription)")
        }
        DTDRemoteConfig.applyConfig()
        // Use here or notify config listeners that actualConfig is ready
        let btnColorRemoteValue = DTDRemoteConfig.config["button_color"].stringValue
        let btnSizeRemoteValue = DTDRemoteConfig.config["button_size"].stringValue
        print("button color: \(btnColorRemoteValue)")
        print("button size: \(btnSizeRemoteValue)")
    }
}
```

{% endtab %}

{% tab title="iOS+macOS (Objective-c)" %}
Set the default values `DTDRemoteConfig.defaults` = `@{"button_color": @"green", @"button_size": @"8"}`

Variable names have to be the same as in the ‘Group settings’ section of the web interface (see ‘web’ above). When the SDK gets included in the A/B test, the devtodev server automatically assigns it to a group.

{% hint style="warning" %}
SDK will not use new variable values that are not set in DTDRemoteConfig.defaults
{% endhint %}

Example of the `DTDRemoteConfig.config`value variants:

<table><thead><tr><th width="172">Key/Value</th><th width="150">defaults</th><th width="150"> Control Group</th><th width="150"> Group A</th><th> Group B</th></tr></thead><tbody><tr><td>button_color</td><td>green</td><td>green</td><td>blue</td><td>gray</td></tr><tr><td>button_size</td><td>8</td><td>8</td><td>12</td><td>14</td></tr></tbody></table>

Example of SDK initialization that includes A/B testing:

{% code fullWidth="true" %}

```objectivec
#import "AppDelegate.h"
#import "DTDAnalytics/DTDAnalytics-Swift.h"

@interface AppDelegate () <DTDRemoteConfigListener>

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Config timeout, optional
    DTDRemoteConfig.remoteConfigWaiting = 10.0;
    // Group timeout, optional
    DTDRemoteConfig.groupDefinitionWaiting = 15.0;
    // Implementation defaults params
    DTDRemoteConfig.defaults = @{@"button_color": @"green",
                                 @"button_size": @"8"};
    [DTDAnalytics applicationKey:@"App ID" abConfigListener:self];
    return YES;
}

// The method is called when the configuration has been received
- (void)onReceivedResult:(enum DTDRemoteConfigReceiveResult)result {
    NSLog(@"Configuration received, result %ld", (long)result);
}

// Called when experiment is found
- (void)onPrepareToChange {
    NSLog(@"Experiment found");
}

- (void)onChangedResult:(enum DTDRemoteConfigChangeResult)result error:(NSError *)error {
    NSLog(@"DTDRemoteConfigChangeResult: %ld", (long)result);
    if (error) {
        NSLog(@"DTDRemoteConfigError: %@", error.localizedDescription);
    }
    [DTDRemoteConfig applyConfig];
    // Use here or notify config listeners that actualConfig is ready
    NSString *btnColorRemoteValue = DTDRemoteConfig.config[@"button_color"].stringValue;
    NSString *btnSizeRemoteValue = DTDRemoteConfig.config[@"button_size"].stringValue;
    NSLog(@"button color: %@", btnColorRemoteValue);
    NSLog(@"button size: %@", btnSizeRemoteValue);
}
@end
```

{% endcode %}
{% endtab %}

{% tab title="Android (Kotlin)" %}
Set the default values `DTDRemoteConfig.defaults` = `mapOf("button_color" to "green", "button_size" to "8")`

Variable names have to be the same as in the ‘Group settings’ section of the web interface (see ‘web’ above). When the SDK gets included in the A/B test, the devtodev server automatically assigns it to a group.

{% hint style="warning" %}
SDK will not use new variable values that are not set in DTDRemoteConfig.defaults
{% endhint %}

Example of the `DTDRemoteConfig.config`value variants:

<table><thead><tr><th width="172">Key/Value</th><th width="150">defaults</th><th width="150"> Control Group</th><th width="150"> Group A</th><th> Group B</th></tr></thead><tbody><tr><td>button_color</td><td>green</td><td>green</td><td>blue</td><td>gray</td></tr><tr><td>button_size</td><td>8</td><td>8</td><td>12</td><td>14</td></tr></tbody></table>

Example of SDK initialization that includes A/B testing:

<pre class="language-kotlin"><code class="lang-kotlin">class MainActivity : AppCompatActivity(), DTDRemoteConfigListener {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // Config timeout, optional
        DTDRemoteConfig.remoteConfigWaiting = 10.0
        // Group timeout, optional
        DTDRemoteConfig.groupDefinitionWaiting = 15.0
        // Implementation defaults params
        DTDRemoteConfig.defaults = mapOf("button_color" to "green", "button_size" to "8")
        // Initialize SDK with ab test
        DTDAnalytics.initializeWithAbTest(appKey = "App ID", context = this, abConfigListener = this)
    } 
    
    /**
     * The method is called when the configuration has been received
     */
    override fun onReceived(result: DTDRemoteConfigReceiveResult) {
       Log.d("D2D", "Configuration received") 
    }
    
    /**
     * Called when experiment is found
     */
    override fun onPrepareToChange() {
       Log.d("D2D", "Experiment found") 
    }

    /**
     * called when SDK receives config
     */
<strong>    override fun onChanged(result: DTDRemoteConfigChangeResult, ex: Exception?) {
</strong>        Log.d("D2D", "DTDRemoteConfigChangeResult: ${result.name}")
        ex?.let {
            Log.d("D2D", "DTDRemoteConfigErr: ${ex.message}")
        }
        DTDRemoteConfig.applyConfig()
        // Use here or notify config listeners that actualConfig is ready
        val btnColorRemoteValue = DTDRemoteConfig.config["button_color"].stringValue
<strong>        val btnSizeRemoteValue = DTDRemoteConfig.config["button_size"].stringValue
</strong><strong>        Log.d("D2D", "button color: $btnColorRemoteValue")
</strong><strong>        Log.d("D2D", "button size: $btnSizeRemoteValue")
</strong><strong>    }
</strong>}
</code></pre>

{% endtab %}

{% tab title="Android (Java)" %}
Set the default values:

```java
HashMap<String, Object> map = new HashMap<>();
map.put("button_color", "green");
map.put("button_size", "8");
DTDRemoteConfig.INSTANCE.setDefaults(map);
```

Variable names have to be the same as in the ‘Group settings’ section of the web interface (see ‘web’ above). When the SDK gets included in the A/B test, the devtodev server automatically assigns it to a group.

{% hint style="warning" %}
SDK will not use new variable values that are not set in DTDRemoteConfig.defaults
{% endhint %}

Example of the `DTDRemoteConfig.config`value variants:

<table><thead><tr><th width="172">Key/Value</th><th width="150">defaults</th><th width="150"> Control Group</th><th width="150"> Group A</th><th> Group B</th></tr></thead><tbody><tr><td>button_color</td><td>green</td><td>green</td><td>blue</td><td>gray</td></tr><tr><td>button_size</td><td>8</td><td>8</td><td>12</td><td>14</td></tr></tbody></table>

Example of SDK initialization that includes A/B testing:

{% code fullWidth="true" %}

```java
public class MainActivityJava extends AppCompatActivity implements DTDRemoteConfigListener {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Config timeout, optional
        DTDRemoteConfig.INSTANCE.setRemoteConfigWaiting(10.0);
        // Group timeout, optional
        DTDRemoteConfig.INSTANCE.setGroupDefinitionWaiting(15.0);
        // Implementation defaults params
        HashMap<String, Object> map = new HashMap<>();
        map.put("button_color", "green");
        map.put("button_size", "8");
        // Initialize SDK with ab test
        DTDRemoteConfig.INSTANCE.setDefaults(map);
    }

    @Override
    public void onReceived(@NonNull DTDRemoteConfigReceiveResult result) {
        Log.d("D2D", "Configuration received");
    }

    @Override
    public void onPrepareToChange() {
        Log.d("D2D", "Experiment found");
    }

    @Override
    public void onChanged(@NonNull DTDRemoteConfigChangeResult result, @Nullable Exception ex) {
        Log.d("D2D", "DTDRemoteConfigChangeResult: " + result.name());
        if (ex != null) {
            Log.d("D2D", "DTDRemoteConfigErr: " + ex);
        }

        DTDRemoteConfig.INSTANCE.applyConfig();
        // Use here or notify config listeners that actualConfig is ready
        String btnColorRemoteValue = DTDRemoteConfig.INSTANCE.getConfig().get("button_color").getStringValue();
        String btnSizeRemoteValue = DTDRemoteConfig.INSTANCE.getConfig().get("button_size").getStringValue();
        Log.d("D2D", "button color: " + btnColorRemoteValue);
        Log.d("D2D", "button size: " + btnSizeRemoteValue);
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="Unity" %}
Set the default values:

```csharp
DTDRemoteConfig.Defaults = new Dictionary<string, object>
{
   {"button_color", "green"},
   {"button_size", 8}
};
```

Variable names have to be the same as in the ‘Group settings’ section of the web interface (see ‘web’ above). When the SDK gets included in the A/B test, the devtodev server automatically assigns it to a group.

{% hint style="warning" %}
SDK will not use new variable values that are not set in DTDRemoteConfig.Defaults
{% endhint %}

Example of the `DTDRemoteConfig.Config` value variants:

<table><thead><tr><th width="172">Key/Value</th><th width="150">defaults</th><th width="150"> Control Group</th><th width="150"> Group A</th><th> Group B</th></tr></thead><tbody><tr><td>button_color</td><td>green</td><td>green</td><td>blue</td><td>gray</td></tr><tr><td>button_size</td><td>8</td><td>8</td><td>12</td><td>14</td></tr></tbody></table>

Example of SDK initialization that includes A/B testing:

```csharp
using System.Collections.Generic;
using DevToDev.Analytics;
using DevToDev.Analytics.ABTest;
using UnityEngine;

namespace AbTesting
{
    public class Example : MonoBehaviour, IDTDRemoteConfigListener
    {
        private void Start()
        {
            DontDestroyOnLoad(this);
            // Config timeout, optional
            DTDRemoteConfig.RemoteConfigWaiting = 10.0;
            // Group timeout, optional
            DTDRemoteConfig.GroupDefinitionWaiting = 15.0;
            // Implementation defaults params
            DTDRemoteConfig.Defaults = new Dictionary<string, object>
            {
                {"button_color", "green"},
                {"button_size", 8}
            };

            // Initialize SDK with ab test
            DTDAnalytics.InitializeWithAbTests("App ID", this);
        }

        // The method is called when the configuration has been received
        public void OnReceived(DTDRemoteConfigReceiveResult result)
        {
            Debug.Log($"Configuration received, result {result}");
        }
        
        // Called when experiment is found
        public void OnPrepareToChange()
        {
            Debug.Log("Experiment found");
        }

        // Called when SDK receives config
        public void OnChanged(DTDRemoteConfigChangeResult result, string exceptionText = null)
        {
            Debug.Log($"DTDRemoteConfigChangeResult: {result}");
            if (exceptionText != null)
            {
                Debug.Log($"DTDRemoteConfigError: {exceptionText}");
            }

            // Use here or notify config listeners that actualConfig is ready
            DTDRemoteConfig.ApplyConfig();
            var buttonColorRemoteValue = DTDRemoteConfig.Config["button_color"].StringValue();
            var buttonSizeRemoteValue = DTDRemoteConfig.Config["button_size"].IntValue();
            Debug.Log($"button color: {buttonColorRemoteValue}");
            Debug.Log($"button size: {buttonSizeRemoteValue}");
        }
    }
}

```

{% endtab %}

{% tab title=".NET + UWP" %}
Set the default values:

```csharp
DTDRemoteConfig.Defaults = new Dictionary<string, object> 
{
  ["button_color"] = "green",
  ["button_size"] = 8
};
```

Variable names have to be the same as in the ‘Group settings’ section of the web interface (see ‘web’ above). When the SDK gets included in the A/B test, the devtodev server automatically assigns it to a group.

{% hint style="warning" %}
SDK will not use new variable values that are not set in DTDRemoteConfig.Defaults
{% endhint %}

Example of the `DTDRemoteConfig.Config` value variants:

<table><thead><tr><th width="172">Key/Value</th><th width="150">defaults</th><th width="150"> Control Group</th><th width="150"> Group A</th><th> Group B</th></tr></thead><tbody><tr><td>button_color</td><td>green</td><td>green</td><td>blue</td><td>gray</td></tr><tr><td>button_size</td><td>8</td><td>8</td><td>12</td><td>14</td></tr></tbody></table>

Example of SDK initialization that includes A/B testing:

```csharp
using DevToDev.Analytics;
using System.Collections.Generic;
using System.Diagnostics;

class MyRemoteConfigListener : IDTDRemoteConfigListener
{
    /**
     * The method is called when the configuration has been received
     */
    public void OnReceived(DTDRemoteConfigReceiveResult result)
    {
      Debug.WriteLine("Configuration has been received!");
    }
    
    /**
     * Called when experiment is found
     */
    public void OnPrepareToChange()
    {
      Debug.WriteLine("Experiment was found!");
    }
    
    /**
     * Called when SDK receives config
     */
    public void OnChanged(DTDRemoteConfigChangeResult result, string error)
    {
      Debug.WriteLine($"Result: {result}");
      if (result == DTDRemoteConfigChangeResult.Failure)
      {
        Debug.WriteLine($"Error: {error}");
      }
      
      DTDRemoteConfig.ApplyConfig();
      DTDRemoteConfig.ApplyConfig();
      var buttonColorRemoteValue = DTDRemoteConfig.Config["button_color"].StringValue;
      var buttonSizeRemoteValue = DTDRemoteConfig.Config["button_size"].IntValue;
      Debug.WriteLine($"button color: {buttonColorRemoteValue}");
      Debug.WriteLine($"button size: {buttonSizeRemoteValue}");
      // Use here or notify config listeners that config is ready
    }
}
  
public static class Program
{
    public static void Main(string[] args)
    {
        // Config timeout, optional
        DTDRemoteConfig.RemoteConfigWaiting = 10;
        
        // Group timeout, optional
        DTDRemoteConfig.GroupDefinitionWaiting = 15;
        
        // Implementation defaults params
        DTDRemoteConfig.Defaults = new Dictionary<string, object> 
        {
          ["button_color"] = "green",
          ["button_size"] = 8
        };
        
        // Create listener
        var listener = new MyRemoteConfigListener();
        
        // Initialize SDK with ab test
        DTDAnalytics.InitializeWithAbTest("App ID", listener);
    }
}
```

{% endtab %}

{% tab title="Web" %}
Set the default values `DTDRemoteConfig.SetDefaults()`

Variable names have to be the same as in the ‘Group settings’ section of the web interface (see ‘web’ above). When the SDK gets included in the A/B test, the devtodev server automatically assigns it to a group.

SDK will not use new variable values that are not set in DTDRemoteConfig.defaults

Example of the `DTDRemoteConfig` value variants:

| **Key/Value** | **defaults** | **Control Group** | **Group A** | **Group B** |
| ------------- | ------------ | ----------------- | ----------- | ----------- |
| button\_color | green        | green             | blue        | gray        |
| button\_size  | 8            | 8                 | 12          | 14          |

Example of SDK initialization that includes A/B testing:

```javascript
window.devtodev.remoteConfig.defaults = {
   button_color: 'green',
   button_size:  8
}
window.devtodev.initializeWithAbTest(
    "App ID", 
    {
        userId: userId,
        logLevel: logLevel,
        trackingAvailability: trackingAvailability,
    },
    {
        onReceived: function(result) {
            console.log('onReceived callback', result)
        },
        onPrepareToChange: function() {
            console.log('onPrepareToChange callback')
        },
        onChanged: function(result, error) {
            console.log('onChanged callback', result, error)
            window.devtodev.remoteConfig.applyConfig()
            var config = window.devtodev.remoteConfig.config // DTDRemoteConfigCollection
            var buttonSizeRemoteValue =  window.devtodev.remoteConfig.config["button_size"].intValue;
            var buttonColorRemoteValue = window.devtodev.remoteConfig.config["button_color"].stringValue;
            console.log("button color: ", buttonColorRemoteValue);
            console.log("button size: ", buttonSizeRemoteValue);
        }
    }
)
```

{% endtab %}

{% tab title="Unreal" %}
Set the default values:

<figure><img src="/files/Zgh1yU61Q1W2LhFLeOS5" alt="" width="375"><figcaption><p>Blueprint</p></figcaption></figure>

```
// Implementation defaults params
FDTDRemoteConfigDefaults defaults;
defaults.StringDefaults.Add("button_color", "green");
defaults.IntegerDefaults.Add("button_size", 8);
```

Variable names have to be the same as in the ‘Group settings’ section of the web interface (see ‘web’ above). When the SDK gets included in the A/B test, the devtodev server automatically assigns it to a group.

{% hint style="warning" %}
SDK will not use new variable values that are not set in DTDRemoteConfig.defaults
{% endhint %}

Example of the `DTDRemoteConfig.config`value variants:

<table><thead><tr><th width="172">Key/Value</th><th width="150">defaults</th><th width="150"> Control Group</th><th width="150"> Group A</th><th> Group B</th></tr></thead><tbody><tr><td>button_color</td><td>green</td><td>green</td><td>blue</td><td>gray</td></tr><tr><td>button_size</td><td>8</td><td>8</td><td>12</td><td>14</td></tr></tbody></table>

Example of SDK initialization that includes A/B testing:

<figure><img src="/files/HnlumPTWHYBuS1rvVGYv" alt="" width="375"><figcaption><p>Blueprint</p></figcaption></figure>

```cpp
#include "DTDAnalytics/Public/DTDAnalyticsBPLibrary.h"
#include "DTDAnalytics/Public/DTDRemoteConfigBPLibrary.h"

UDTDAnalyticsBPLibrary::SetLogLevel(EDTDLogLevel::Debug);

// Config timeout, optional
UDTDRemoteConfigBPLibrary::SetRemoteConfigWaiting(10.0f);
// Group timeout, optional
UDTDRemoteConfigBPLibrary::SetGroupDefinitionWaiting(15.0f);

// Implementation defaults params
FDTDRemoteConfigDefaults defaults;
defaults.StringDefaults.Add("button_color", "green");
defaults.IntegerDefaults.Add("button_size", 8);

UDTDRemoteConfigBPLibrary::SetDefaults(defaults);

// The method is called when the configuration has been received
const auto onConfigReceive = new FDTDRemoteConfigReceiveResultDelegate();
onConfigReceive->BindLambda([=](EDTDRemoteConfigReceiveResult result)
{	
	UE_LOG(LogTemp, Warning, TEXT("Configuration received, result %s"), *UEnum::GetValueAsString(result));
});

// Called when experiment is found
const auto onPrepareToChange = new FDTDRemoteConfigPrepareToChangeDelegate();
onPrepareToChange->BindLambda([=]()
{
	UE_LOG(LogTemp, Warning, TEXT("Experiment found"));
});

// Called when SDK receives config
const auto onConfigChange = new FDTDRemoteConfigChangeResultDelegate();
onConfigChange->BindLambda([=](EDTDRemoteConfigChangeResult result, const FString& error)
{
	UE_LOG(LogTemp, Warning, TEXT("DTDRemoteConfigChangeResult: %s"), *UEnum::GetValueAsString(result));
	if (!error.IsEmpty()) {
		UE_LOG(LogTemp, Warning, TEXT("DTDRemoteConfigError: %s"), *error);
	}

	// Use here or notify config listeners that actualConfig is ready
	UDTDRemoteConfigBPLibrary::ApplyConfig();

	FString ButtonColorRemoteValue = UDTDRemoteConfigBPLibrary::GetRemoteConfigValue("button_color").StringValue;
        int32 buttonSizeRemoteValue = UDTDRemoteConfigBPLibrary::GetRemoteConfigValue("button_size").IntegerValue;
	UE_LOG(LogTemp, Warning, TEXT("button color: : %s"), *ButtonColorRemoteValue);
	UE_LOG(LogTemp, Warning, TEXT("button size:: %d"), buttonSizeRemoteValue);
});

// Initialize SDK with ab test
UDTDAnalyticsBPLibrary::InitializeWithAbTest("App ID", *onConfigChange, *onPrepareToChange, *onConfigReceive);
```

{% endtab %}

{% tab title="Godot" %}
Set the default values `DTDRemoteConfig.SetDefaults()`

Variable names have to be the same as in the ‘Group settings’ section of the web interface (see ‘web’ above). When the SDK gets included in the A/B test, the devtodev server automatically assigns it to a group.

{% hint style="warning" %}
SDK will not use new variable values that are not set in DTDRemoteConfig.defaults
{% endhint %}

Example of the `DTDRemoteConfig` value variants:

<table><thead><tr><th width="172">Key/Value</th><th width="150">defaults</th><th width="150"> Control Group</th><th width="150"> Group A</th><th> Group B</th></tr></thead><tbody><tr><td>button_color</td><td>green</td><td>green</td><td>blue</td><td>gray</td></tr><tr><td>button_size</td><td>8</td><td>8</td><td>12</td><td>14</td></tr></tbody></table>

Example of SDK initialization that includes A/B testing:

<pre class="language-gdscript"><code class="lang-gdscript">func _ready():
    	# Config timeout, optional
	DTDRemoteConfig.SetRemoteConfigWaiting(10)
	# Group timeout, optional
	DTDRemoteConfig.SetGroupDefinitionWaiting(10)
    	# Implementation defaults params
	var defaults = GDDTDRemoteConfigDefaults.new()
	defaults.AddStringValue("button_color", "green")
	defaults.AddIntegerValue("button_size", 8)
	DTDRemoteConfig.SetDefaults(defaults)
    	# Initialize SDK with ab test
	DTDAnalytics.SetLogLevel(GDDTDLogLevel.Debug)
	DTDAnalytics.InitializeWithConfigWithAbTest(
		"appKey",
		onRemoteConfigChange,
		onRemoteConfigPrepareToChange,
		onRemoteConfigReceive)

# The method is called when the configuration has been received
func onRemoteConfigReceive(result: GDDTDRemoteConfigReceiveResult.ReceiveResult):
	match result:
		GDDTDRemoteConfigReceiveResult.ReceiveResult.Failure:
			print("onRemoteConfigReceive result = Failure")
			
		GDDTDRemoteConfigReceiveResult.ReceiveResult.Success:
			print("onRemoteConfigReceive result = Success")
			
		GDDTDRemoteConfigReceiveResult.ReceiveResult.Empty:
			print("onRemoteConfigReceive result = Empty")
			
		_:
			print("onRemoteConfigReceive result = Unknown")
<strong>
</strong><strong># Called when experiment is found		
</strong>func onRemoteConfigPrepareToChange():
	print("Experiment found")
			
# Called when SDK receives config
func onRemoteConfigChange(result: GDDTDRemoteConfigChangeResult.ChangeResult, error: String):
	if !error.is_empty(): 
		print("error = " + error)
		
	match result:
		GDDTDRemoteConfigChangeResult.ChangeResult.Failure:
			print("onRemoteConfigChange result = Failure")	
			
		GDDTDRemoteConfigChangeResult.ChangeResult.Success:
			
			print("onRemoteConfigChange result = Success")
		_: 
			print("onRemoteConfigChange result = Unknown")
			
	DTDRemoteConfig.ApplyConfig()
	
	# Use here or notify config listeners that actualConfig is ready
	var btnColorRemoteValue = DTDRemoteConfig.GetRemoteConfigValue("button_color").GetStringValue()
	var btnSizeRemoteValue = DTDRemoteConfig.GetRemoteConfigValue("button_size").GetStringValue()
	print("button color: " + btnColorRemoteValue)
	print("button size: " + btnSizeRemoteValue)
</code></pre>

{% endtab %}
{% endtabs %}

## Some features of A/B testing

When the SDK finds a suitable test(s), it calls the `onPrepareToChange` method and reports it. Then the SDK starts a timer for 30 seconds and sends a request to the devtodev server. The server returns an experiment number and information about a group. Or the server answers that the user can not participate in the test. This process takes some time. The request and answer can take as little as milliseconds, or much longer (30 seconds maximum) if, for example, the device’s internet connection is bad. Below, you can find several use cases that may help you to solve this problem if your app’s interface is sensitive to waiting for the config from devtodev.

## **Description of external interfaces**

### **DTDRemoteConfig**

When working with this class, the SDK provides synchronization of threads that aims at providing safe operation taking into account the asynchrony of the SDK.

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

| Property                            | Description                                                                                                                                                                                                                   |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `remoteConfigWaiting: Double`       | <p>Wait time for A/B test configuration. </p><p></p><p>Default value - 0.0 (measured in seconds)</p>                                                                                                                          |
| `groupDefinitionWaiting: Double`    | <p>Wait time for test group. </p><p></p><p>Default value - 10.0 (measured in seconds)</p>                                                                                                                                     |
| `defaults: [String, Any]`           | Variables and their default values                                                                                                                                                                                            |
| `config: DTDRemoteConfigCollection` | <p>Wrapper for remote parameters in the form of a collection. It allows access to the configuration values by using the subscripting syntaxis. </p><p></p><p>Actual variables and their values for working with A/B tests</p> |
| `applyConfig()`                     | It applies the A/B testing configuration. After the call, the default parameters get matched with the group parameters.                                                                                                       |
| `resetConfig()`                     | It cancels a suitable or running test                                                                                                                                                                                         |
| `cacheTestExperiment()`             | A debug method for saving a test experiment after restarting the application                                                                                                                                                  |
| {% endtab %}                        |                                                                                                                                                                                                                               |

{% tab title="iOS+macOS (Objective-C)" %}

| Property                                | Description                                                                                                                                                                                                                   |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `remoteConfigWaiting: double`           | <p>Wait time for A/B test configuration. </p><p></p><p>Default value - 0.0 (measured in seconds)</p>                                                                                                                          |
| `groupDefinitionWaiting: double`        | <p>Wait time for test group. </p><p></p><p>Default value - 10.0 (measured in seconds)</p>                                                                                                                                     |
| `defaults: NSDictionary<NSString *,id>` | Variables and their default values                                                                                                                                                                                            |
| `config: DTDRemoteConfigCollection`     | <p>Wrapper for remote parameters in the form of a collection. It allows access to the configuration values by using the subscripting syntaxis. </p><p></p><p>Actual variables and their values for working with A/B tests</p> |
| `(void) applyConfig`                    | It applies the A/B testing configuration. After the call, the default parameters get matched with the group parameters.                                                                                                       |
| `(void) resetConfig`                    | It cancels a suitable or running test                                                                                                                                                                                         |
| `(void) cacheTestExperiment`            | A debug method for saving a test experiment after restarting the application                                                                                                                                                  |
| {% endtab %}                            |                                                                                                                                                                                                                               |

{% tab title="Android (Kotlin)" %}

| Property                            | Description                                                                                                                                                                                                                   |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `remoteConfigWaiting: Double`       | <p>Wait time for A/B test configuration. </p><p></p><p>Default value - 0.0 (measured in seconds)</p>                                                                                                                          |
| `groupDefinitionWaiting: Double`    | <p>Wait time for test group. </p><p></p><p>Default value - 10.0 (measured in seconds)</p>                                                                                                                                     |
| `defaults: Map<String, Any>`        | Variables and their default values                                                                                                                                                                                            |
| `config: DTDRemoteConfigCollection` | <p>Wrapper for remote parameters in the form of a collection. It allows access to the configuration values by using the subscripting syntaxis. </p><p></p><p>Actual variables and their values for working with A/B tests</p> |
| `applyConfig()`                     | It applies the A/B testing configuration. After the call, the default parameters get matched with the group parameters.                                                                                                       |
| `resetConfig()`                     | It cancels a suitable or running test                                                                                                                                                                                         |
| `cacheTestExperiment()`             | A debug method for saving a test experiment after restarting the application                                                                                                                                                  |
| {% endtab %}                        |                                                                                                                                                                                                                               |

{% tab title="Android (Java)" %}

<table><thead><tr><th width="362">Property</th><th>Description</th></tr></thead><tbody><tr><td><code>setRemoteConfigWaiting</code>()</td><td><p>Wait time for A/B test configuration. </p><p></p><p>Default value - 0.0 (measured in seconds)</p></td></tr><tr><td><code>getRemoteConfigWaiting</code>()</td><td><p>Wait time for test group. </p><p></p></td></tr><tr><td><code>setGroupDefinitionWaiting</code>()</td><td><p>Wait time for test group.</p><p></p><p>Default value - 10.0 (measured in seconds)</p></td></tr><tr><td><code>getGroupDefinitionWaiting</code>()</td><td>Get time for test group.</td></tr><tr><td><code>setDefaults(Map&#x3C;String, ? extends Object></code>)</td><td>Set map of variables and their default values</td></tr><tr><td><code>Map&#x3C;String,Object> getDefaults()</code></td><td>Get map of variables and their default values</td></tr><tr><td><code>DTDRemoteConfigCollection getConfig()</code></td><td><p>Wrapper for remote parameters in the form of a collection. It allows access to the configuration values by using the subscripting syntaxis.</p><p></p><p>Actual variables and their values for working with A/B tests</p></td></tr><tr><td><code>applyConfig()</code></td><td>It applies the A/B testing configuration. After the call, the default parameters get matched with the group parameters.</td></tr><tr><td><code>resetConfig()</code></td><td>It cancels a suitable or running test</td></tr><tr><td><code>cacheTestExperiment()</code></td><td>A debug method for saving a test experiment after restarting the application</td></tr></tbody></table>
{% endtab %}

{% tab title="Unity" %}

| Property                                                        | Description                                                                                                                                                                                                                   |
| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `double RemoteConfigWaiting {get;set;}`                         | <p>Wait time for A/B test configuration. </p><p></p><p>Default value - 0.0 (measured in seconds)</p>                                                                                                                          |
| `double GroupDefinitionWaiting {get;set}`                       | <p>Wait time for test group. </p><p></p><p>Default value - 10.0 (measured in seconds)</p>                                                                                                                                     |
| `Dictionary<string, object> DTDRemoteConfig.Defaults {get;set}` | Variables and their default values                                                                                                                                                                                            |
| `DTDRemoteConfigCollection DTDRemoteConfig.Config`              | <p>Wrapper for remote parameters in the form of a collection. It allows access to the configuration values by using the subscripting syntaxis. </p><p></p><p>Actual variables and their values for working with A/B tests</p> |
| `void ApplyConfig()`                                            | It applies the A/B testing configuration. After the call, the default parameters get matched with the group parameters.                                                                                                       |
| `void ResetConfig()`                                            | It cancels a suitable or running test                                                                                                                                                                                         |
| `void CacheTestExperiment()`                                    | A debug method for saving a test experiment after restarting the application                                                                                                                                                  |
| {% endtab %}                                                    |                                                                                                                                                                                                                               |

{% tab title=".Net + UWP" %}

| Property                                | Description                                                                                                                                                                                                                   |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RemoteConfigWaiting: double`           | <p>Wait time for A/B test configuration. </p><p></p><p>Default value - 0.0 (measured in seconds)</p>                                                                                                                          |
| `GroupDefinitionWaiting: double`        | <p>Wait time for test group. </p><p></p><p>Default value - 10.0 (measured in seconds)</p>                                                                                                                                     |
| `Defaults: IDictionary<String, object>` | Variables and their default values                                                                                                                                                                                            |
| Config: DTDRemoteConfigCollection       | <p>Wrapper for remote parameters in the form of a collection. It allows access to the configuration values by using the subscripting syntaxis. </p><p></p><p>Actual variables and their values for working with A/B tests</p> |
| `ApplyConfig()`                         | It applies the A/B testing configuration. After the call, the default parameters get matched with the group parameters.                                                                                                       |
| `ResetConfig()`                         | It cancels a suitable or running test                                                                                                                                                                                         |
| `CacheTestExperiment()`                 | A debug method for saving a test experiment after restarting the application                                                                                                                                                  |
| {% endtab %}                            |                                                                                                                                                                                                                               |

{% tab title="Web" %}

<table><thead><tr><th width="233.17578125">Property</th><th>Description</th></tr></thead><tbody><tr><td><code>remoteConfigWaiting</code></td><td><p>Wait time for A/B test configuration.</p><p>Default value - 0 (measured in seconds)</p></td></tr><tr><td><code>groupDefinitionWaiting</code></td><td><p>Wait time for test group.</p><p>Default value - 15 (measured in seconds)</p></td></tr><tr><td><code>defaults</code></td><td>Variables and their default values</td></tr><tr><td><code>config</code></td><td><p>Wrapper for remote parameters in the form of a collection. It allows access to the configuration values by using the subscripting syntaxis.</p><p>Actual variables and their values for working with A/B tests</p></td></tr><tr><td><code>applyConfig()</code></td><td>It applies the A/B testing configuration. After the call, the default parameters get matched with the group parameters.</td></tr><tr><td><code>resetConfig()</code></td><td>It cancels a suitable or running test</td></tr><tr><td><code>cacheTestExperiment()</code></td><td>A debug method for saving a test experiment after restarting the application</td></tr></tbody></table>
{% endtab %}

{% tab title="Unreal" %}

<table><thead><tr><th width="362">Property</th><th>Description</th></tr></thead><tbody><tr><td><code>SetRemoteConfigWaiting(float value)</code></td><td><p>Wait time for A/B test configuration. </p><p></p><p>Default value - 0.0 (measured in seconds)</p></td></tr><tr><td><code>float GetRemoteConfigWaiting()</code></td><td><p>Wait time for test group. </p><p></p></td></tr><tr><td><code>SetGroupDefinitionWaiting(float value)</code></td><td><p>Wait time for test group.</p><p></p><p>Default value - 10.0 (measured in seconds)</p></td></tr><tr><td><code>GetGroupDefinitionWaiting()</code></td><td>Get time for test group.</td></tr><tr><td><code>SetDefaults(const FDTDRemoteConfigDefaults&#x26; defaults)</code></td><td>Set USTRUCT with variables and their default values</td></tr><tr><td><code>TMap&#x3C;FString, FDTDRemoteConfigValue> GetConfig()</code></td><td>Actual variables and their values for working with A/B tests</td></tr><tr><td><code>FDTDRemoteConfigValue GetRemoteConfigValue(const FString&#x26; key)</code></td><td>Return actual value for variable key</td></tr><tr><td><code>bool HasKey(const FString&#x26; key)</code></td><td>Verify if there are any values associated with the variable key in the remote configuration.</td></tr><tr><td><code>ApplyConfig()</code></td><td>It applies the A/B testing configuration. After the call, the default parameters get matched with the group parameters.</td></tr><tr><td><code>ResetConfig()</code></td><td>It cancels a suitable or running test</td></tr><tr><td><code>CacheTestExperiment()</code></td><td>A debug method for saving a test experiment after restarting the application</td></tr></tbody></table>
{% endtab %}

{% tab title="Godot" %}

<table><thead><tr><th width="368">Property</th><th>Description</th></tr></thead><tbody><tr><td><code>void SetRemoteConfigWaiting(int value)</code></td><td><p>Wait time for A/B test configuration. </p><p></p><p>Default value - 0 (measured in seconds)</p></td></tr><tr><td><code>int GetRemoteConfigWaiting()</code></td><td><p>Wait time for test group. </p><p></p></td></tr><tr><td><code>void SetGroupDefinitionWaiting(int value)</code></td><td><p>Wait time for test group.</p><p></p><p>Default value - 10 (measured in seconds)</p></td></tr><tr><td><code>int GetGroupDefinitionWaiting()</code></td><td>Get time for test group.</td></tr><tr><td><code>void SetDefaults(defaults: GDDTDRemoteConfigDefaults)</code></td><td>Set <code>GDDTDRemoteConfigDefaults</code> with variables and their default values</td></tr><tr><td><code>GDDTDRemoteConfigValue GetRemoteConfigValue(key: String)</code></td><td>Return actual value for variable key</td></tr><tr><td><code>bool HasKey(key: String)</code></td><td>Verify if there are any values associated with the variable key in the remote configuration.</td></tr><tr><td><code>void ApplyConfig()</code></td><td>It applies the A/B testing configuration. After the call, the default parameters get matched with the group parameters.</td></tr><tr><td><code>void ResetConfig()</code></td><td>It cancels a suitable or running test</td></tr><tr><td><code>void CacheTestExperiment()</code></td><td>A debug method for saving a test experiment after restarting the application</td></tr></tbody></table>
{% endtab %}
{% endtabs %}

### DTDRemoteConfigCollection

Wrapper for remote parameters collection. Enables access to configuration values by using subscripting syntax.

{% tabs %}
{% tab title="Web" %}

<table><thead><tr><th width="183.40234375">Method</th><th></th></tr></thead><tbody><tr><td><code>hasKey(key)</code></td><td>Return true if current configuration has value for a key.</td></tr><tr><td><code>values</code></td><td>Returns the plain object of the current configuration with key/values pairs</td></tr></tbody></table>
{% endtab %}

{% tab title="All other SDKs" %}

| Method                |                                                           |
| --------------------- | --------------------------------------------------------- |
| `hasKey(key: String)` | Return true if current configuration has value for a key. |
| {% endtab %}          |                                                           |
| {% endtabs %}         |                                                           |

### DTDRemoteConfigValue

Wrapper for working with remote configuration variables. It represents a method for data source identification, as well as methods for presenting values in the form of various data types.

| type                            | Description                                            |
| ------------------------------- | ------------------------------------------------------ |
| `DTDRemoteConfigSource.Default` | The variable is set by default.                        |
| `DTDRemoteConfigSource.Remote`  | The variable is set by the test group.                 |
| `DTDRemoteConfigSource.Empty`   | The variable is not found in the remote configuration. |

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

| Property       | type    | Description                           |
| -------------- | ------- | ------------------------------------- |
| `stringValue`  | String? | Gets the value as an optional string. |
| `floatValue`   | Float   | Gets the value as a Float.            |
| `doubleValue`  | Double  | Gets the value as a Double.           |
| `Int32Value`   | Int32   | Gets the value as an Int32.           |
| `Int64Value`   | Int64   | Gets the value as an Int64.           |
| `integerValue` | Int     | Gets the value as an Int.             |
| `boolValue`    | Bool    | Gets the value as a Bool.             |
| {% endtab %}   |         |                                       |

{% tab title="iOS+macOS (Objective-C)" %}

| Property       | type      | Description                           |
| -------------- | --------- | ------------------------------------- |
| `stringValue`  | NSString  | Gets the value as an optional string. |
| `floatValue`   | float     | Gets the value as a Float.            |
| `doubleValue`  | double    | Gets the value as a Double.           |
| `Int32Value`   | long      | Gets the value as an long.            |
| `Int64Value`   | long long | Gets the value as an long long.       |
| `integerValue` | NSInteger | Gets the value as an NSInteger.       |
| `boolValue`    | BOOL      | Gets the value as a Bool.             |
| {% endtab %}   |           |                                       |

{% tab title="Android (Kotlin)" %}

| Property       | type    | Description                           |
| -------------- | ------- | ------------------------------------- |
| `stringValue`  | String? | Gets the value as an optional string. |
| `floatValue`   | Float   | Gets the value as a Float.            |
| `doubleValue`  | Double  | Gets the value as a Double.           |
| `intValue`     | Int     | Gets the value as an int.             |
| `longValue`    | Long    | Gets the value as an long.            |
| `booleanValue` | Boolean | Gets the value as a Bool.             |
| {% endtab %}   |         |                                       |

{% tab title="Android (Java)" %}

| Property          | type    | Description                           |
| ----------------- | ------- | ------------------------------------- |
| `getStringValue`  | String  | Gets the value as an optional string. |
| `getFloatValue`   | float   | Gets the value as a Float.            |
| `getDoubleValue`  | double  | Gets the value as a Double.           |
| `getIntValue`     | int     | Gets the value as an Int.             |
| `getLongValue`    | long    | Gets the value as an Long.            |
| `getBooleanValue` | Boolean | Gets the value as a Boolean.          |
| {% endtab %}      |         |                                       |

{% tab title="Unity" %}

| Property      | type   | Description                           |
| ------------- | ------ | ------------------------------------- |
| `StringValue` | string | Gets the value as an optional string. |
| `FloatValue`  | float  | Gets the value as a float.            |
| `DoubleValue` | double | Gets the value as a double.           |
| `LongValue`   | long   | Gets the value as an long.            |
| `IntValue`    | int    | Gets the value as an int.             |
| `BoolValue`   | bool   | Gets the value as a bool.             |
| {% endtab %}  |        |                                       |

{% tab title=".Net + UWP" %}

| Property      | type   | Description                           |
| ------------- | ------ | ------------------------------------- |
| `StringValue` | String | Gets the value as an optional string. |
| `FloatValue`  | float  | Gets the value as a float.            |
| `DoubleValue` | double | Gets the value as a double.           |
| `Int16Value`  | Int16  | Gets the value as an Int16            |
| `Int32Value`  | Int32  | Gets the value as an Int32            |
| `Int64Value`  | Int64  | Gets the value as an Int64            |
| `BoolValue`   | bool   | Gets the value as a bool.             |
| {% endtab %}  |        |                                       |

{% tab title="Web" %}

| Property      | Type    | Description                                                                                           |
| ------------- | ------- | ----------------------------------------------------------------------------------------------------- |
| `StringValue` | String  | Gets the value as an optional string.                                                                 |
| `FloatValue`  | Number  | Gets the value as a float number                                                                      |
| `DoubleValue` | Number  | Gets the value as a double number                                                                     |
| `LongValue`   | String  | <p>Gets the value as a long</p><p>NB: <code>can be simulated only via string in JavaScript</code></p> |
| `IntValue`    | Number  | Gets the value as a number                                                                            |
| `BoolValue`   | Boolean | Gets the value as a boolean                                                                           |
| {% endtab %}  |         |                                                                                                       |

{% tab title="Unreal" %}

<table><thead><tr><th width="197.66666666666666">Property</th><th>type</th><th>Description</th></tr></thead><tbody><tr><td><code>StringValue</code></td><td>FString</td><td>Gets the value as an string.</td></tr><tr><td><code>FloatValue</code></td><td>float</td><td>Gets the value as a float.</td></tr><tr><td><code>LongValue</code></td><td>int64</td><td>Gets the value as an long.</td></tr><tr><td><code>IntValue</code></td><td>int32</td><td>Gets the value as an int.</td></tr><tr><td><code>BoolValue</code></td><td>bool</td><td>Gets the value as a bool.</td></tr><tr><td><code>Source</code></td><td>EDTDRemoteConfigSource</td><td>Gets source of the value.</td></tr></tbody></table>
{% endtab %}

{% tab title="Godot" %}

<table><thead><tr><th width="197.66666666666666">Property</th><th>type</th><th>Description</th></tr></thead><tbody><tr><td><code>GetStringValue</code></td><td>String</td><td>Gets the value as an string.</td></tr><tr><td><code>GetFloatValue</code></td><td>float</td><td>Gets the value as a float.</td></tr><tr><td><code>GetIntValue</code></td><td>int</td><td>Gets the value as an int.</td></tr><tr><td><code>GetBoolValue</code></td><td>bool</td><td>Gets the value as a bool.</td></tr><tr><td><code>GetSource</code></td><td>GDDTDRemoteConfigSource.Source</td><td>Gets source of the value.</td></tr></tbody></table>
{% endtab %}
{% endtabs %}

### DTDRemoteConfigListener

{% hint style="warning" %}
Not applied to Web SDK
{% endhint %}

It implements methods that report the status of A/B tests.

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

<table><thead><tr><th width="338.4332022087659">Method</th><th>Description</th></tr></thead><tbody><tr><td><code>onReceived(result: DTDRemoteConfigReceiveResult)</code></td><td>It is triggered every time the SDK loads an A/B test configuration. If the remoteConfigWaiting has a default value (null), <code>onReceived</code> does not get called</td></tr><tr><td><code>onPrepareToChange()</code></td><td>It notifies the developer about coming changes in the A/B test configuration.</td></tr><tr><td><p><code>onChanged(result: DTDRemoteConfigChangeResult,</code> </p><p><code>error: Error?)</code></p></td><td>It notifies the developer that the configuration has changed. Or about the reason why the configuration could not change.</td></tr></tbody></table>
{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

<table><thead><tr><th width="417.3913043478261">Method</th><th>Description</th></tr></thead><tbody><tr><td><p><code>-(void)onReceivedResult:</code></p><p><code>(enum DTDRemoteConfigReceiveResult)result</code></p></td><td>It is triggered every time the SDK loads an A/B test configuration. If the remoteConfigWaiting has a default value (null), <code>onReceived</code> does not get called</td></tr><tr><td><code>-(void)onPrepareToChange</code></td><td>It notifies the developer about coming changes in the A/B test configuration.</td></tr><tr><td><p><code>- (void)onChangedResult:</code></p><p><code>(enum DTDRemoteConfigChangeResult)result</code></p><p><code>error:(NSError *)error</code></p></td><td>It notifies the developer that the configuration has changed. Or about the reason why the configuration could not change.</td></tr></tbody></table>
{% endtab %}

{% tab title="Android (Kotlin)" %}

| Method                                                           | Description                                                                                                                                                 |
| ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `onReceived(result: DTDRemoteConfigReceiveResult)`               | It is triggered every time the SDK loads an A/B test configuration. If the remoteConfigWaiting has a default value (null), `onReceived` does not get called |
| `onPrepareToChange()`                                            | It notifies the developer about coming changes in the A/B test configuration.                                                                               |
| `onChanged(result: DTDRemoteConfigChangeResult, ex: Exception?)` | It notifies the developer that the configuration has changed. Or about the reason why the configuration could not change.                                   |
| {% endtab %}                                                     |                                                                                                                                                             |

{% tab title="Android (Java)" %}

<table><thead><tr><th width="355">Method</th><th>Description</th></tr></thead><tbody><tr><td><code>onReceived(DTDRemoteConfigReceiveResult result)</code></td><td>It is triggered every time the SDK loads an A/B test configuration. If the remoteConfigWaiting has a default value (null), <code>onReceived</code> does not get called</td></tr><tr><td><code>onPrepareToChange()</code></td><td>It notifies the developer about coming changes in the A/B test configuration.</td></tr><tr><td><code>onChanged( DTDRemoteConfigChangeResult result, Exception ex)</code></td><td>It notifies the developer that the configuration has changed. Or about the reason why the configuration could not change.</td></tr></tbody></table>
{% endtab %}

{% tab title="Unity" %}
**IDTDRemoteConfigListener**

<table><thead><tr><th width="338.4332022087659">Method</th><th>Description</th></tr></thead><tbody><tr><td><code>void OnReceived(DTDRemoteConfigReceiveResult result)</code>;</td><td>It is triggered every time the SDK loads an A/B test configuration. If the remoteConfigWaiting has a default value (null), <code>OnReceived</code> does not get called</td></tr><tr><td><code>void OnPrepareToChange();</code></td><td>It notifies the developer about coming changes in the A/B test configuration.</td></tr><tr><td><code>void OnChanged(DTDRemoteConfigChangeResult result, string exceptionText = null)</code>;</td><td>It notifies the developer that the configuration has changed. Or about the reason why the configuration could not change.</td></tr></tbody></table>
{% endtab %}

{% tab title=".Net + UWP" %}
**IDTDRemoteConfigListener**

<table><thead><tr><th width="338.4332022087659">Method</th><th>Description</th></tr></thead><tbody><tr><td><code>OnReceived(DTDRemoteConfigReceiveResult result)</code></td><td>It is triggered every time the SDK loads an A/B test configuration. If the remoteConfigWaiting has a default value (null), <code>OnReceived</code> does not get called</td></tr><tr><td><code>OnPrepareToChange()</code></td><td>It notifies the developer about coming changes in the A/B test configuration.</td></tr><tr><td><code>OnChanged(DTDRemoteConfigChangeResult result, string error)</code></td><td>It notifies the developer that the configuration has changed. Or about the reason why the configuration could not change.</td></tr></tbody></table>
{% endtab %}

{% tab title="Unreal" %}
**FDTDRemoteConfigReceiveResultDelegate**

It is triggered every time the SDK loads an A/B test configuration. If the remoteConfigWaiting has a default value (null), `OnReceived` does not get called

<figure><img src="/files/akXc40uTajeE7sB9Y2E0" alt="" width="375"><figcaption><p>Blueprint</p></figcaption></figure>

```
DECLARE_DELEGATE_OneParam(FDTDRemoteConfigReceiveResultDelegate, EDTDRemoteConfigReceiveResult);
```

**FDTDRemoteConfigPrepareToChangeDelegate**

It notifies the developer about coming changes in the A/B test configuration.

<figure><img src="/files/olgKHmIZbqebC9WSD6st" alt="" width="375"><figcaption><p>Blueprint</p></figcaption></figure>

```
DECLARE_DELEGATE(FDTDRemoteConfigPrepareToChangeDelegate);
```

**FDTDRemoteConfigChangeResultDelegate**

It notifies the developer that the configuration has changed. Or about the reason why the configuration could not change.

<figure><img src="/files/GjoyAWbI95Ay4k8eC3rc" alt="" width="375"><figcaption><p>Blueprint</p></figcaption></figure>

```
DECLARE_DELEGATE_TwoParams(FDTDRemoteConfigChangeResultDelegate, EDTDRemoteConfigChangeResult, const FString&);
```

{% endtab %}

{% tab title="Godot" %}

<table><thead><tr><th width="338.4332022087659">Method</th><th>Description</th></tr></thead><tbody><tr><td><code>onRemoteConfigReceive(result: GDDTDRemoteConfigReceiveResult.ReceiveResult):</code></td><td>It is triggered every time the SDK loads an A/B test configuration. If the remoteConfigWaiting has a default value (null), <code>onReceived</code> does not get called</td></tr><tr><td><code>onRemoteConfigPrepareToChange()</code></td><td>It notifies the developer about coming changes in the A/B test configuration.</td></tr><tr><td><code>onRemoteConfigChange(result: GDDTDRemoteConfigChangeResult.ChangeResult, error: String)</code></td><td>It notifies the developer that the configuration has changed. Or about the reason why the configuration could not change.</td></tr></tbody></table>
{% endtab %}
{% endtabs %}

### **DTDRemoteConfigReceiveResult**

It is triggered every time the SDK loads the configuration of A/B tests.

<table><thead><tr><th width="358.1573840725806">type</th><th>Description</th></tr></thead><tbody><tr><td><code>DTDRemoteConfigReceiveResult.Success</code></td><td>It is triggered when the configuration of the A/B test is received.</td></tr><tr><td><code>DTDRemoteConfigReceiveResult.Failure</code></td><td>It is triggered if the SDK did not manage to get the A/B test configurations for the time period set in remoteConfigWaiting.</td></tr><tr><td><code>DTDRemoteConfigReceiveResult.Empty</code></td><td>It is triggered when the configuration of the A/B test is received, but the list of experiments is empty.</td></tr></tbody></table>

### **DTDRemoteConfigChangeResult**

Notifies whether the configuration of the A/B test has been changed.

<table><thead><tr><th width="351.527920995396">type</th><th>Description</th></tr></thead><tbody><tr><td><code>DTDRemoteConfigReceiveResult.Success</code></td><td>Configuration is changed.</td></tr><tr><td><code>DTDRemoteConfigReceiveResult.Failure</code></td><td>Configuration change attempt failed.</td></tr></tbody></table>

Data options returned by the `configChanged` method signature

<table><thead><tr><th width="272.3333333333333">DTDRemoteConfigChangeResult</th><th>Exception message</th><th>Description</th></tr></thead><tbody><tr><td><code>DTDRemoteConfigChangeResult.Success</code></td><td>null</td><td>When receiving an offer from the backend.</td></tr><tr><td><code>DTDRemoteConfigChangeResult.Success</code></td><td>null</td><td>When launching the SDK with a previously started test.</td></tr><tr><td><code>DTDRemoteConfigChangeResult.Success</code></td><td>null</td><td>When replacing the user with another user who previously started the test.</td></tr><tr><td><code>DTDRemoteConfigChangeResult.Failure</code></td><td>[A/B-Test Module] The Server refused to conduct the test.</td><td>If the server refused to provide a test available for participation.</td></tr><tr><td><code>DTDRemoteConfigChangeResult.Failure</code></td><td>[A/B-Test Module] Offer from devtodev not received within the allotted N seconds.</td><td>The backend offer is returned after a deadline e.g. due to a bad internet connection.</td></tr><tr><td><code>DTDRemoteConfigChangeResult.Failure</code></td><td>[A/B-Test Module] Offer from devtodev not received within the allotted N seconds.</td><td>The SDK was unable to receive an offer from the backend within N seconds e.g, duea bad internet connection or network problems.</td></tr><tr><td><code>DTDRemoteConfigChangeResult.Failure</code></td><td>[A/B-Test Module] In the process of receiving an offer from the server, the user was changed. Offer has been canceled.</td><td>When replacing the user with another user at the time of receiving an offer.</td></tr><tr><td><code>DTDRemoteConfigChangeResult.Failure</code></td><td>[A/B-Test Module] Offer refused, because application went into the background.</td><td>If a suitable test is found but the user made the app go into the background before the offer was received.</td></tr></tbody></table>


# Working with A/B tests in devtodev

A/B testing is the best way to challenge your hypotheses. A/B testing is essentially an experiment where you show your users different variants of the app at random and then analyze the results to determine which variation performed better.&#x20;

{% embed url="<https://www.youtube.com/watch?v=XMWhnEWC0aY>" %}
A/B testing overview
{% endembed %}

{% hint style="success" %}
Check out this guide to [A/B testing essentials and strategies](https://www.devtodev.com/resources/articles/a-b-testing-essentials-strategies-metrics-and-ai)!
{% endhint %}

In devtodev, you can work with A/B testing in the ‘A/B Testing’ section on the app level. All tests are stored in one table. Besides basic information about each test, you can see its status. There are five types of status:&#x20;

* **Draft** – draft of an unexecuted test.&#x20;
* **Stopped** – the test was stopped before completion. It’s not possible to restart the test. If you want to restart it, make a copy of the test and launch it.&#x20;
* **In progress** – the test is currently in progress.&#x20;
* **Finished: No winner** – test results determined that there was no winner.&#x20;
* **Finished: Success** – test results determined a winner group.

<figure><img src="/files/J1b3AyX3f1YR9lTa5FNI" alt=""><figcaption></figcaption></figure>

## How to prepare for A/B testing&#x20;

Before creating a test in the devtodev interface, you need to set variables through the SDK and use the methods for launching A/B tests. Use certain classes to set the variables and their default values. In case the variables are involved with the test, their values will vary depending on the group defined by the server. If the app will be offline and won’t be able to present the test to the user, he will see default values and the app will continue to function correctly.

[Here](/integration/integration-of-sdk-v2/a-b-testing/description-of-a-b-testing-on-the-sdk-side) you can find more information about SDK configuration (about setting variables and methods for launching A/B tests).

## Creating an A/B test

To go to the test creation wizard, open the desired devtodev project, navigate to the ‘A/B Testing’ tab and click the ‘+ Add new A/B test’ button.&#x20;

You have opened the segment creation wizard that consists of five steps:

### **1. Experiment name**

Enter a unique name of the test and its description. Try to make the description of the test as detailed as possible: its hypothesis, audience, description of the test groups, target metric, desired outcome, etc. Or simply insert a link to the test description. Check out this article to [learn more about test planning](https://www.devtodev.com/education/articles/en/351/a-b-testing-in-liveops).

### **2. Set the audience**

In this section, you need to create test assignment rules and define the audience size. Use the ‘Filter your audience’ and ‘Triggering event’ sections to set the assignment rules.

**Filter your audience** – use this option to define user properties that are needed to be included into the test. The devtodev SDK which is integrated into the application, will use the filters to select the audience whose current properties match the test requirements.&#x20;

<figure><img src="/files/gaCEy2691aLE6Kt2fhVP" alt=""><figcaption></figcaption></figure>

In this example, all paying users will participate in the test.

If you use several filters at once, only users or devices (this depends on the selected user identification method) that meet all conditions will participate in the test.

Set a **triggering event** if you want your test to include only the users who performed a certain event.&#x20;

<figure><img src="/files/Yk3DJeuT8IbqETo8I3gN" alt=""><figcaption></figcaption></figure>

In this example, the devtodev SDK will include the user or device (this depends on the selected user identification method) in the test when the SDK receives information that the said user or device reached the fourth level.

To each trigger event, you can add more parameters that are related to the event. Events have different lists of additional parameters (see [Events](/integration/integration-of-sdk-v2/setting-up-events)).&#x20;

{% hint style="info" %}
Please note that you can’t use an event that you sent to devtodev via API as a trigger event.
{% endhint %}

The selected filters and trigger events cannot be altered after the start of the experiment.

The filters and trigger events become available for audience configuration after at least one event/property is received via the SDK, processed and accounted for in the devtodev analytics.

When applying both filters and trigger events, all conditions have to be met for the user/device to be included into the test.

**Audience fraction** – use this option to define the percentage or an absolute number of users out of those selected by the filter and/or completed the trigger event who will participate in the test. If the initial audience size is not enough for drawing firm conclusions, you can change it even after the test begins. <br>

<figure><img src="/files/zSsnE2wNl8ewQJpTdiaX" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
Please note that one user can participate in only one test at a time.
{% endhint %}

If you need to run several tests in parallel, then:&#x20;

* You need to create non-overlapping test audiences&#x20;
* If your audience overlaps, you need to configure the audience fraction so that part of the audience will be included into each test (e.g. 50% of the overlapping audience gets included into each of the two tests).&#x20;

If you know the number of users that you need to achieve a statistically significant result, insert it in the ‘Max number of observation’ cell.

![](https://lh3.googleusercontent.com/V6Zx3AXFj4R59WegEo9B9XoLkPVmQpMloe249Ttc2DsffxsVaG5Bg77HO2aaKK1VwYRF4u4m-Ly4A1iYBE19gZIH1L0On5JFUb8BaxM3-r5KXkkA6Ek_qLTWO7p2hZ99-kXsmTItB8WIwp8qWTk)

In this example, 100% of users who completed more than three levels at the time of a sign up will be included in the test.

A user can be excluded from the test only in two cases:&#x20;

* The test time is up.&#x20;
* The test is stopped.

### 3. Goals&#x20;

In this section, you can define the goal of your A/B test – metrics for analysis, criteria for stopping the experiment for a group, and the duration of the experiment.

You can set up one ‘Primary metric’ and no more than five ‘Secondary metrics’ for each test. The ‘Primary metric’ is used to assess the test result and to calculate statistical significance of the obtained result. ‘Secondary metrics’ are optional and do not take part in the final result assessment. However, they can improve the quality of the analysis and prove that the implemented changes did not influence other key metrics of the app.

For example, you can select one of the following as a secondary metric:&#x20;

* One of the fundamental metrics (ARPU, Paying conversion, Day-N retention, etc.)&#x20;
* User engagement with an event.&#x20;
* Average number of times an event was completed (per user).

Below, you can set the ‘Estimated experiment duration’ (days). The test will be stopped after the set number of days. You can also automatically stop the test execution in case the winning group is defined – simply check the ‘Stop the experiment when there is a winning group’ box.

If you don’t see any sense in continuing the test or you want to change the group settings and restart it, you are free to change the duration of the test or even stop it anytime during its course.

To calculate the size of the test audience, use any of the [Size Calculators](https://www.evanmiller.org/ab-testing/sample-size.html). To use them, first estimate the current value of the Primary metric and then define the result that you expect to get from the tested changes. In addition, you can set up several user experience funnels and display their results in the test report. This will give you additional information about how successful the test has been.

![](https://lh6.googleusercontent.com/guJx42gIGov-oDbbP6RmAwAXA-XQTst4PMqEuv5_PthX2vCavJ_Z_lp05Xem1FL3wZPTXX8qkpnGOFWbq_kvL3iHJDcFS2xqNU_vQtafOsTuLaejIMaEmPLcYFrs3RoQmiYzY_wynQK2KPJ6res)

The main goal of the test above is to improve conversion to payment. However you can use the same method to test the conversion to trial or to ARPU.

### 4. Group settings&#x20;

One of the most crucial steps is setting up test groups and variables. You create a config containing various groups and their parameters. After the devtodev SDK reports that the user has been successfully included in the experiment, one of the groups becomes available in the app via the SDK.

By default, there are two groups available to you: Group A and a control group. You can increase the number of groups to maximum 4 in a single test by clicking the ‘+Add group’ button.&#x20;

The control group usually includes users as they currently are. This way, you can test other variants to see the change in their metrics, relative to the same metrics at the moment. For each group you need to define a set of variables that is composed of the name of the variable and its value. The variables have to be defined inside of your app – they are supposed to grant your users different experiences.<br>

![](https://lh6.googleusercontent.com/SvsbpPhKE3qwOyFyCqBvNpY4Nwkd7fNz5CCdqTHJyV_qmmmfXly8-LS-P7sWJQY9mJz6fxaXzCJHX1CtE5VdFubOrmXJILTnPdq8_Ow_NrT7LpgGsG4kbpvnRPpnqBgjDfk6JiWop6F6mgpNQEU)

In the above example, you can see three groups: the control group (it has default parameters) and two more groups that have other parameters for the button\_color and button\_size variables. The test will be focused on defining the most favorable size and color of the button. If one of the groups wins, it may lead to the change of interface for all users of the app.

When the app is launching, the SDK defines the test that the user will participate in. Then he is randomly assigned to a test group and the SDK applies all the variables defined for this group.

### 5. Checking of the group settings&#x20;

To make sure that all the test groups are set up correctly and that the app is handling the selected variables the right way, we highly recommend you to test the current test settings. In this section, you can check how the A/B test configuration runs on test devices, manually determine relevant groups for them and also check how the design and app behavior change in different groups.

Click ‘+Add test device’ to add a test device using an Advertising ID / User ID / devtodev ID or select it from the list of test devices in the current devtodev Space. After that, select a user group that you want to test on and click ‘Start checking’.

![](https://lh4.googleusercontent.com/OESeDxfKObqhhmtjqTrLNH52FVCFcLNE9WokE1CrPPLVRn7h2X6Kq6EDYw3jgm4E0N_GuJrS-UyeoH6kGwzwFBA4XmlpD7OXU0_haJzzhzq_sOkV9sZubf9xte0dCRD-ukcK7nqj13JQH9hNJEo)

The settings of the selected group will be applied to the selected test device. From this moment on, the test device will start the test for the selected group and they will not wait for meeting the entry conditions that you have specified above.

![](https://lh6.googleusercontent.com/6SrkQXk_r9CMIQZI9rNyicqUwEUgcQbTghj-06rnAuFsfDAAkxP10Qyh5NITpp95a63Rd_6_p-GYQEtknTIuPL3Hxt3dcZMQChNKlYuRUMUQXfiU4RtR5nLWApVt1nK--Oa8RtRpmMSl-A2aCK4)

Test devices do not save the information about the active test or the group. After you successfully finish testing one group, select the next one for the same test device and click ‘Restart checking’.

{% hint style="info" %}
To be able to access the active test and its group at a test device after restarting the app, use the `DTDRemoteConfig.сacheTestExperiment()` method before initializing the SDK.
{% endhint %}

After you check all group settings on the test devices, you can launch the test for the entire selected audience or save it as a draft.

{% hint style="info" %}
A maximum of 8 A/B tests can be run simultaneously in one project.
{% endhint %}

## Finish the test&#x20;

The test can have several outcomes. Let's look at them in more detail.

**Force stop and test delete**

It may so happen that you’ve launched an incorrectly configured test and now you need to stop it. To do this, you can select the required test by clicking on it in the list of experiments.&#x20;

* To stop the test (full stop, no chance to resume) – open the A/B test report and click on the edit icon in the upper right corner. The test editing wizard will open. Click on the Stop Test button at the bottom of the wizard page.&#x20;
* To remove the A/B test from the list – open the test editing wizard. At the bottom of the wizard page, click the Delete Test button. Please note that you can delete only the tests that were stopped or completed. The created A/B tests stay in the project until the user deletes them.

![](https://lh5.googleusercontent.com/rxy2zhCUcrC6YND_0ci-E6EY4Zslu2a9bSdwAG2n0e8FOYFivAw5XxszPTZ8Tb5o8K61e3dsIvke5D423lLaLsH5FRdh8omLxszf10yUMx045xsKbwGcacRpTrPCcVF0Ly7yE1tUix4ASfQuCdU)

The SDK updates the A/B test config only during initialization. If the deleted experiment was previously activated on any device, then when the SDK is initialized, it will be available for activation. After the config gets updated, the SDK will remove this test from the database but will not report it via external interfaces.

This is intended to avoid changing the app settings that were received from the experiment config at the beginning of the session (e.g, the UI that the user is currently interacting with). The next time the app starts, the test will be deleted during SDK initialization.

{% hint style="warning" %}
Do not update the app interface and behavior when the user interacts with it. Do not use the network to receive default parameters. It is better to define them in the app
{% endhint %}

**Test completion using the specified criteria**

* If you check the ‘Stop the experiment when there is a winning group’ box at the third step of the A/B test creation process, the test will automatically stop if **‘Probability to be best’ of one of the groups is larger than 95%.**\
  *This metric (Probability to be best) can be considered to be a Bayesian approach.*\
  \
  This value is auto-calculated for each group based on the selected Primary metric. If you want to stop the test when reaching a higher number (e.g. 99%), you can change the test duration and continue with its execution until you reach the desired outcome.<br>
* The test can finish when it reaches the end of the time period specified at the ‘Goals’ step in the ‘Estimated experiment duration → duration days’ section which is responsible for the test duration. For example, you set ‘duration days’ as 3. This means that the entire audience has only three days since the test creation to be included into the test and participate in it.\
  \
  When the app is launched and the SDK is initialized, it will compare the current time with the end time of the test. If the experiment time is up, the SDK will erase the test from the database and the users will not be able to receive any data. If the test time has run out when the app has been used, the SDK will not respond.&#x20;

This is intended to avoid changing the app settings that were received from the experiment config at the beginning of the session (e.g, the UI that the user is currently interacting with). The next time the app starts, the test will be deleted as described above.

### Test completion report

During the test execution, a report on the test results will be built and updated in real time. In the upper block, you can find all the basic information about the current state:

* Current test status&#x20;
* Name of the group with the highest ‘Probability to be best’ and its value&#x20;
* Number of groups, total number of users in the test, and number of users in each group&#x20;
* Experiment time frame

Below you can see a graph. Its horizontal axis represents calendar days starting from the test start date, while the vertical axis represents the value of the selected metric for each of the groups. You can select the displayed metric in the top left corner of the graph. These are:

* The Primary and Secondary metrics selected in the wizard&#x20;
* Probability to be best&#x20;
* A/B test audience

![](https://lh6.googleusercontent.com/m4EG4wvCTsABhLtO0JGUyo5A_71ZdlPWaj8Bu_udk1UYBeUSL1xwmQmKDQkowpH4BgIXgxnya7Bu8JLn-MGKRkM0ojjqdSdeH77AzJSe5xGAp4VTTxBMgl6r4Vc81NkJl7FyCjEclBnFU-ze5JI)

Underneath you can find a table with aggregated values for each of the metrics in each test group and their fluctuations relative to the control group. The ‘Probability to be best’ is also calculated for each metric including the Secondary. This way you can make sure that all the tested changes do not influence other metrics in a negative way. \
After that you can see the funnels configured in the A/B test creation wizard. They contain data on the number of users at each funnel stage, conversion rate from one stage to another, and the ‘Probability to be best’ for conversion from the first to the last stage.

![](https://lh4.googleusercontent.com/cZf4I7LhkBomWfvVaOire22kP8S_DxtuNM2l_X7zk3B0ALh281SPPHPwW6Xnr5zN-VN2umKUpkSpZSla9LrvuZNVY-B-WeX9BULLK6XlWmSzIjj4bWz5Z-C4ZpBFB0hMO01GW53lyvuv4bhvtSI)

### A/B test groups

When a user gets included into a test group, he is automatically marked with the ID of this group.&#x20;

If you want to drill down even more and understand a subtle difference in metrics and behavior of the users, you can use these user groups as filters in any devtodev reports. Simply go to the report filters, open the ‘Segments’ tab, and select the required segment.

![](https://lh4.googleusercontent.com/lUV51p1NZzcxkf1yn0J3rYURwh47-WBul0fhtZVbPiDJowDjCBjjJ5gXIYVrqbAVjNDf4VqcdIt_Jk6XBPe3EkdXvnZ8lCfmbfJE2Azy4nFS_mbuSaMYLYBolce_WWBdrGTe2DiNNzIcS2hJLkk)

* *A/B test segment values are saved as a separate user property (‘A/B test groups’) in the user card at the moment the user gets included into an A/B test group.*&#x20;
* *The user can not enroll into two A/B tests at the same time. He can be included into one test and after it completes, be included into another. In this case, the ‘ A/B test groups’ field will contain names of the two groups.*

![](https://lh4.googleusercontent.com/xHD3QIR05ufsRqc_Llr00sWOCkYCeYjtVAlgAaAc0jW1FjfAJp3M-sXNq7hRZ4TMDZM5mE7qjnVmf3DIugMQuqzO6_w2xs6diTVwi41QcjtXyp619UbminBNM0D6LFo1Fi7PN5rR1XOW5a7r_ds)


# A/B testing examples

## **Example №1**

### Hypothesis

Our analytics data show that most of the people make a purchase during their first session. However, only 70% of users open our in-app store during the first session. We hypothesize that if we add a purchase screen to the tutorial then 100% of users will face it and the number of purchases during the first session will increase.

### Test criteria

The criteria for test inclusion is starting the tutorial. The `DTDAnalytics.tutorial(step: Int)` event with step = -1 is the trigger.

![](https://lh4.googleusercontent.com/maVAk84FVwBv47Y42SBnrXd0On-gV_6U-0wQtZHlO5nFd8OXMUHmeszocvujCU7TYNSlyM13Hw9JJt27Ka7jGfMsKQkXaVQXnGoaklWe0SgBMq3bbYdvVabqzscLvI92sN8_JYc1eE3dadL2IjTOQlc)

### Groups&#x20;

Control group: a 10 step tutorial, no purchase screens&#x20;

Group А: an 11 step tutorial. At step number 5, we will offer a special offer.

![](https://lh3.googleusercontent.com/4UueLHy8PNG0chIgavEEpBytsy0nq3NLq561adxAhgv4CpH658jwRtdiTr6W6CccFf1PTPInR8Di7Vz759ii8J92w6h-K_lnHzmAfpC_oBtUxEr_VjmE4waLoTKp1w5uYK5bckYPPEuV9sO_KtTYLXo)

### **Implementation**

{% hint style="danger" %}
**This integration manual is only for SDK versions below 2.6.0 / Unity 3.10.0.**&#x20;

If you are using SDK 2.6.0 / Unity 3.10.0 and higher, please refer to the [updated integration manual](/integration/integration-of-sdk-v2/remote-configuration/rc-integration).
{% endhint %}

{% tabs fullWidth="true" %}
{% tab title="iOS+macOS (Swift)" %}

```swift
// Timer constants
struct Constants {
    static let waitGroupConst = 10.0
}

// Value keys
enum ValueKey: String {
  case showStore
}

class AppConfig {
    static func loadDefaults() {
        let appDefaults: [String: Any] = [
            ValueKey.showStore.rawValue: "false"
        ]
        // Set default values
        DTDRemoteConfig.defaults = appDefaults
    }
    
    static func bool(forKey key: ValueKey) -> Bool {
        return DTDRemoteConfig.config[key.rawValue].boolValue
    }
}

class AppLogic {
    var timer: Timer
    // Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
    func startTutorial() {
        // Send a trigger event that indicates tutorial start
        DTDAnalytics.tutorial(step: -1)
    }
    
    func nextTutotrialStep(_ currentStep: Int) {
        DTDAnalytics.tutorial(step: currentStep)
        if AppConfig.bool(forKey: .showStore) && currentStep == 5 {
            // Offer purchasing of a special offer
        }
    }
}

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication,
        willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Set the maximum time of waiting for an A/B test group
        DTDRemoteConfig.groupDefinitionWaiting = Constants.waitGroupConst
        // Set default values
        AppConfig.loadDefaults()
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.initializeWithAbTest(applicationKey: "App ID",
                                          configuration: config,
                                          abConfigListener: self)
    }
}

extension AppLogic: DTDRemoteConfigListener {
    // Process the result of waiting for A/B test configuration
    func onReceived(result: DTDRemoteConfigReceiveResult) {
        // It is not used in current example
    }

    // Prepare the app UI for changing the remote configuration
    func onPrepareToChange() {
        // Use the main app thread because you are getting ready for working with the interface
        DispatchQueue.main.async { [weak self] in
            // Display the download progress indicator
            self?.showActivityIndicator()
            // Add a timer that will forcibly remove the download progress indicator
            self?.timer = Timer.scheduledTimer(withTimeInterval: Constants.waitGroupConst, repeats: false) { [weak self] _ in
                self?.hideActivityIndicator()
            }
        }
    }
    
    // Apply the values of the assigned group
    func onChanged(result: DTDRemoteConfigChangeResult, error: Error?) {
        defer {
            // Hide the download progress indicator
            DispatchQueue.main.async { [weak self] in
                self?.timer.invalidate();
                self?.hideActivityIndicator()
            }
        }

        switch result {
        case .success:
            // Apply new values
            DTDRemoteConfig.applyConfig()

        case .failure:
            // Error processing
            if let error = error {
                print(error.localizedDescription)
            }

        @unknown default:
            break
        }
    }
}
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

```objectivec
// Constants .h + .m
@interface Constants : NSObject
// Timer constants
extern double const waitGroupConst;
// Value keys
extern NSString * const showStore;
@end

@implementation Constants
double const waitGroupConst = 10.0;
NSString * const showStore = @"showStore";
@end

// AppConfig .h + .m
@interface AppConfig: NSObject
+(void) loadDefaults;
+(BOOL) getBoolForKey:(NSString *) key;
@end

@implementation AppConfig
+(void)loadDefaults {
    NSDictionary *appDefaults = @{
        showStore: @NO
    };

    // Set default values
    DTDRemoteConfig.defaults = appDefaults;
}

+(BOOL) getBoolForKey:(NSString *) key {
    return DTDRemoteConfig.config[key].boolValue;
}
@end

// AppLogic .h + .m
@interface AppLogic : UIViewController <DTDRemoteConfigListener>
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation AppLogic

- (void)viewDidLoad {
    [super viewDidLoad];
    [self startTutorial];
}

// Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
- (void) startTutorial {
    // Send a trigger event that indicates tutorial start
    [DTDAnalytics tutorialStep: -1];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
        [self nextTutotrialStep:5];
    });
}

- (void) nextTutotrialStep:(NSInteger) currentStep {
    [DTDAnalytics tutorialStep: currentStep];
    if ([AppConfig getBoolForKey:showStore] && currentStep == 5) {
        // Offer purchasing of a special offer
    }
}

// Process the result of waiting for A/B test configuration
- (void)onReceivedResult:(enum DTDRemoteConfigReceiveResult)result {
    // It is not used in current example
}

// Prepare the app UI for changing the remote configuration
- (void)onPrepareToChange {
    // Use the main app thread because you are getting ready for working with the interface
    __weak AppLogic *weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        // Display the progress indicator
        [weakSelf showActivityIndicator];
        // Add a timer that will forcibly remove the progress indicator
        self.timer = [NSTimer scheduledTimerWithTimeInterval:waitGroupConst repeats:false block:^(NSTimer *timer){
            [weakSelf hideActivityIndicator];
        }];
    });
}

// Apply the values of the assigned group
- (void)onChangedResult:(enum DTDRemoteConfigChangeResult)result error:(NSError *)error {
    switch (result) {
        case DTDRemoteConfigChangeResultSuccess:
            // Apply new values
            [DTDRemoteConfig applyConfig];
            break;

        case DTDRemoteConfigChangeResultFailure:
            // Error processing
            if (error) {
                NSLog(@"DTDRemoteConfigError: %@", error.localizedDescription);
            }

        default:
            break;
    }

    // Hide the progress indicator
    __weak AppLogic *weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.timer invalidate];
        [weakSelf hideActivityIndicator];
    });
}

- (void)showActivityIndicator {
    // Display the progress indicator
}

- (void)hideActivityIndicator {
    // Hide the progress indicator
}
@end

// AppDelegate .h + .m
@interface AppDelegate ()
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    AppLogic *appLogicController = [[UIStoryboard storyboardWithName:@"Main" bundle:nil] instantiateViewControllerWithIdentifier:@"AppLogic"];
    UINavigationController *navController = [[UINavigationController alloc]initWithRootViewController:appLogicController];
    self.window.rootViewController = navController;
    [self.window makeKeyAndVisible];

    // Group timeout, optional
    DTDRemoteConfig.groupDefinitionWaiting = waitGroupConst;
    // Implementation defaults params
    [AppConfig loadDefaults];
    [DTDAnalytics applicationKey:appKey abConfigListener:appLogicController];

    return YES;
}
@end
```

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
class Constants {
    // Timer constants
    companion object {
        const val waitGroupConst = 10.0
        const val waitGroupConstInMilliseconds = 10000L
    }
}

// Value keys
enum class ValueKey(val value: String) {
    ShowStore("showStore")
}

class AppConfig {
    companion object {
        fun loadDefaults() {
            val appDefaults = mapOf<String, Any>(ValueKey.ShowStore.value to "false")
            DTDRemoteConfig.defaults = appDefaults
        }

        fun bool(key: ValueKey): Boolean {
            return DTDRemoteConfig.config[key.value].booleanValue
        }
    }
}

class AppLogic {
    // Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
    fun startTutorial() {
        // Send a trigger event that indicates tutorial start
        DTDAnalytics.tutorial(step = -1)
    }

    fun nextTutorialStep(currentStep: Int) {
        DTDAnalytics.tutorial(step = currentStep)
        if (AppConfig.bool(ValueKey.ShowStore) && currentStep == 5) {
            // Offer purchasing of a special offer
        }
    }
}

class MainActivity : AppCompatActivity(), DTDRemoteConfigListener {
    //activityIndicator control timer
    var timer: TimerTask? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState, persistentState)
        setContentView(R.layout.activity_main)

        // Set the maximum time of waiting for an A/B test group
        DTDRemoteConfig.groupDefinitionWaiting = Constants.waitGroupConst
        // Set default values
        AppConfig.loadDefaults()
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.initializeWithAbTest(
            appKey = "App ID",
            context = this,
            abConfigListener = this
        )
    }

    // Process the result of waiting for A/B test configuration
    override fun onReceived(result: DTDRemoteConfigReceiveResult) {
        // It is not used in current example
    }

    // Prepare the app UI for changing the remote configuration
    override fun onPrepareToChange() {
        // Use the main app thread because you are getting ready for working with the interface
        runOnUiThread {
            // Display the download progress indicator
            this.showActivityIndicator()
        }
        // Add a timer that will forcibly remove the download progress indicator
        timer = Timer("ActivityIndicator").schedule(Constants.waitGroupConstInMilliseconds) {
            runOnUiThread {
                this.hideActivityIndicator()
            }
        }
    }

    override fun onChanged(result: DTDRemoteConfigChangeResult, ex: Exception?) {
        when (result) {
            DTDRemoteConfigChangeResult.Success -> {
                // Apply new values
                DTDRemoteConfig.applyConfig()
            }
            DTDRemoteConfigChangeResult.Failure -> {
                // Error processing
                ex?.let { Log.e("TAG", ex.toString()) }
            }
        }

        this.timer?.cancel()
        this.timer = null
        
        runOnUiThread {
            this.hideActivityIndicator()
        }
    
```

{% endtab %}

{% tab title="Android (Java)" %}

```java
class Constants {
    // Timer constants
    final static double waitGroupConst = 10.0;
    final static long waitGroupConstInMilliseconds = 10000L;
}

enum ValueKey {
    ShowStore("showStore");

    private final String stringValue;

    ValueKey(String toString) {
        stringValue = toString;
    }

    @Override
    public String toString() {
        return stringValue;
    }
}

class AppConfig {
    static void loadDefaults() {
        HashMap<String, Object> map = new HashMap<>();
        map.put(ValueKey.ShowStore.toString(), "false");
        DTDRemoteConfig.INSTANCE.setDefaults(map);
    }

    static Boolean bool(ValueKey key) {
        return DTDRemoteConfig.INSTANCE.getConfig().get(key.toString()).getBooleanValue();
    }
}

class AppLogic {
    // Tutorial open event (e.g. by clicking the 'start tutorial' button)
    void startTutorial() {
        // Send a trigger event that indicates tutorial start
        DTDAnalytics.INSTANCE.tutorial( -1);
    }

    void nextTutorialStep(int currentStep) {
        DTDAnalytics.INSTANCE.tutorial(currentStep);
        if (AppConfig.bool(ValueKey.ShowStore) && currentStep == 5) {
            // Offer purchasing of a special offer
        }
    }
}

class MainActivity extends AppCompatActivity implements DTDRemoteConfigListener {
    //activityIndicator control timer
    Timer timer = null;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Set the maximum time of waiting for an A/B test group
        DTDRemoteConfig.INSTANCE.setGroupDefinitionWaiting(Constants.waitGroupConst);
        // Set default values
        AppConfig.loadDefaults();
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.INSTANCE.initializeWithAbTest("App ID", this, this);
    }

    // Process the result of waiting for A/B test configuration
    @Override
    public void onReceived(@NonNull DTDRemoteConfigReceiveResult result) {
        // It is not used in current example
    }

    // Prepare the app UI for changing the remote configuration
    @Override
    public void onPrepareToChange() {
        // Use the main app thread because you are getting ready for working with the interface
        runOnUiThread(() -> {
            // Display the download progress indicator
            showActivityIndicator();
        });

        timer = new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                runOnUiThread(() -> hideActivityIndicator());
            }
        }, Constants.waitGroupConstInMilliseconds);
    }

    @Override
    public void onChanged(@NonNull DTDRemoteConfigChangeResult result, @Nullable Exception ex) {
        if (result == DTDRemoteConfigChangeResult.Success) {
            // Apply new values
            DTDRemoteConfig.INSTANCE.applyConfig();
        }

        if (result == DTDRemoteConfigChangeResult.Failure) {
            // Apply new values
            DTDRemoteConfig.INSTANCE.applyConfig();
        }

        if (timer != null) {
            timer.cancel();
            timer = null;
        }

        runOnUiThread(() -> hideActivityIndicator());
    }
}
```

{% endtab %}

{% tab title="Unity" %}

<pre class="language-csharp"><code class="lang-csharp">// Timer constants
public static class Constants
{
    public const float WAIT_GROUP_TIME = 10.0f;
}

// Value keys
public enum ValueKey
{
    showStore
}

public class AppConfig
{
    public void LoadDefaults()
    {
        var appDefaults = new Dictionary&#x3C;string, object>
        {
            {ValueKey.showStore.ToString(), false}
        };
        DTDRemoteConfig.Defaults = appDefaults;
    }

    public bool GetBool(ValueKey key) => DTDRemoteConfig.Config[key.ToString()].BoolValue();
}

public class AppLogic : MonoBehaviour
{
    private readonly AppConfig _appConfig = new AppConfig();
    private const string APP_KEY = "App ID";
    private SimpleUI _simpleUI;
    private void Start()
    {
        DontDestroyOnLoad(this);
        _simpleUI = FindObjectOfType&#x3C;SimpleUI>();
        if (_simpleUI == null) throw new NullReferenceException("UIManager not found.");
        // Set the maximum time of waiting for an A/B test group
        DTDRemoteConfig.GroupDefinitionWaiting = Constants.WAIT_GROUP_TIME;
        // Set default values
        _appConfig.LoadDefaults();
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.InitializeWithAbTests(
            appKey: APP_KEY,
            configListener: this);
    }

<strong>    // Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
</strong><strong>    // Send a trigger event that indicates tutorial start
</strong>    public void StartTutorial() => DTDAnalytics.Tutorial(-1);

    public void NextTutorialStep(int step)
    {
        DTDAnalytics.Tutorial(step);
        if (_appConfig.GetBool(ValueKey.showStore) &#x26;&#x26; step == 5)
        {
            // Offer purchasing of a special offer
            Store.Instance.ShowSpecialOffer();
        }
    }

<strong>    // Process the result of waiting for A/B test configuration
</strong>    public void OnReceived(DTDRemoteConfigReceiveResult result)
    {
        // It is not used in current example
    }

    // Prepare the app UI for changing the remote configuration
    public void OnPrepareToChange()
    {
        // Display the download progress indicator
        _simpleUI.ShowLoadingIndicator(Constants.WAIT_GROUP_TIME);
    }
    
    // Apply the values of the assigned group
    public void OnChanged(DTDRemoteConfigChangeResult result, string exceptionText = null)
    {
        // Hide the download progress indicator
        _simpleUI.HideLoadingIndicator();
        switch (result)
        {
            case DTDRemoteConfigChangeResult.Failure:
                // Error processing
                Debug.LogError(exceptionText);
                break;
            case DTDRemoteConfigChangeResult.Success:
                // Apply new values
                DTDRemoteConfig.ApplyConfig();
                break;
        }
    }
}

</code></pre>

{% endtab %}

{% tab title=".Net + UWP" %}

<pre class="language-csharp"><code class="lang-csharp">// Timer constants
static class Constants
{
    public const float WaitGroupConst = 10.0f;
}

// Value keys
static class ValueKeys
{
    public const string ShowStore = "showStore";
}

static class AppConfg
{
    public static void LoadDafaults()
    {
        DTDRemoteConfig.Defaults = new Dictionary&#x3C;string, object>
        {
          [ValueKeys.ShowStore] = false
        };
    }
    
    public static bool GetBoolValue(string key)
    {
        return DTDRemoteConfig.Config[key].BoolValue;
    }
}

class Application : IDTDRemoteConfigListener
{
    public void Run()
    {
       // Set the maximum time of waiting for an A/B test group
        DTDRemoteConfig.GroupDefinitionWaiting = Constants.WaitGroupConst;
        // Set default values
        AppConfg.LoadDafaults();
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.InitializeWithAbTest("App ID", this);
        // Your code to show UI
    }

    // Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
    public void StartTutorial()
    {
        // Send a trigger event that indicates tutorial start
        DTDAnalytics.Tutorial(-1);
    }
    
    public void NextTutotrialStep(int step)
    {
        DTDAnalytics.Tutorial(step);
        if (AppConfg.GetBoolValue(ValueKeys.ShowStore) &#x26;&#x26; step == 5)
        {
            // Offer purchasing of a special offer (method do a job in UI thread)
            UI.ShowSpecialOffer();
        }
    }
    
    // Apply the values of the assigned group
    public void OnChanged(DTDRemoteConfigChangeResult result, string error)
    {
        Debug.WriteLine($"[App-ABTests] OnChanged({result}, {error})");
        switch (result)
        {
            case DTDRemoteConfigChangeResult.Failure:
                // Error processing
                break;
            case DTDRemoteConfigChangeResult.Success:
                // Apply new values
                DTDRemoteConfig.ApplyConfig();
                break;
            default:
                break;
        }
        
        // Hide the download progress indicator(method do a job in UI thread)
        UI.HideLoadingIndicator();
    }

    // Prepare the app UI for changing the remote configuration
    public void OnPrepareToChange()
    {
        Debug.WriteLine($"[App-ABTests] OnPrepareToChange()");
        // Display the download progress indicator (method do a job in UI thread)
        UI.ShowLoadingIndicator();
        // Add a timer that will forcibly remove the download progress indicator
        TimerUtil.CallWithDelay(Constants.WaitGroupConst, () =>
        {
            // Hide the download progress indicator(method do a job in UI thread)
            UI.HideLoadingIndicator();
        });
    }

<strong>    // Process the result of waiting for A/B test configuration
</strong>    public void OnReceived(DTDRemoteConfigReceiveResult result)
    {
        // It is not used in current example
        Debug.WriteLine($"[App-ABTests] OnReceived({result})");
    }
}
</code></pre>

{% endtab %}

{% tab title="Web" %}

```javascript
devtodev.remoteConfig.defaults = {
   showStore: false,
}
devtodev.remoteConfig.groupDefinitionWaiting = 10
devtodev.initializeWithAbTest(
    "App ID", 
    {
        userId: userId,
        logLevel: logLevel,
        trackingAvailability: trackingAvailability,
    },
    {
        // Process the result of waiting for A/B test configuration
        onReceived: function(result) {
            // It is not used in current example
        },
        // Prepare the app UI for changing the remote configuration
        onPrepareToChange: function() {
            // Display the progress indicator
            ui.showSpinner()
        },
        // Apply the values of the assigned group
        onChanged: function(result, error) {
            ui.hideSpinner()
            switch (result) {
              case DTDRemoteConfigChangeResult.Failure:
                  // Error processing
                  console.error(error);
                  break;
              case DTDRemoteConfigChangeResult.Success:
                  // Apply new values
                  devtodev.remoteConfig.applyConfig()
                  break;
            }
        }
    }
)
function startTutorial() {
    devtodev.tutorial(parseInt(-1))
}
function setTutorialStep(step) {
    devtodev.tutorial(parseInt(step))
    var showStore = window.devtodev.remoteConfig.config['showStore'].boolValue
    if (showStore && step == 5) {
          // Offer purchasing of a special offer
          store.showSpecialOffer();
    }
}
```

{% endtab %}

{% tab title="Unreal" %}

<pre class="language-cpp"><code class="lang-cpp">// Timer constant
<strong>float WAIT_GROUP_TIME = 15.0f;
</strong>
void SomeLogicClass::Start() {
    // Set the maximum time of waiting for an A/B test group
    UDTDRemoteConfigBPLibrary::SetGroupDefinitionWaiting(WAIT_GROUP_TIME);

    // Set default values
    FDTDRemoteConfigDefaults Defaults;
    Defaults.BoolDefaults.Add("showStore", false);
    UDTDRemoteConfigBPLibrary::SetDefaults(Defaults);

    const auto onConfigReceive = new FDTDRemoteConfigReceiveResultDelegate();
    onConfigReceive->BindUObject(this, &#x26;SomeLogicClass::OnConfigReceive);

    const auto onPrepareToChange = new FDTDRemoteConfigPrepareToChangeDelegate();
    onPrepareToChange->BindUObject(this, &#x26;SomeLogicClass::OnPrepareToChange);

    const auto onConfigChange = new FDTDRemoteConfigChangeResultDelegate();
    onConfigChange->BindUObject(this, &#x26;SomeLogicClass::OnConfigChange);

    // Initialize the SDK for working with A/B testing
    UDTDAnalyticsBPLibrary::InitializeWithAbTest("App ID", *onConfigChange, *onPrepareToChange, *onConfigReceive);
}

// Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
void SomeLogicClass::StartTutorial() {
    // Send a trigger event that indicates tutorial start
    UDTDAnalyticsBPLibrary::Tutorial(-1);
}

void SomeLogicClass::NextTutorialStep(int32 step)
{
    UDTDAnalyticsBPLibrary::Tutorial(step);
    bool isNeedShowStore = UDTDRemoteConfigBPLibrary::GetRemoteConfigValue("showStore").BoolValue;
    if (isNeedShowStore &#x26;&#x26; step == 5)
    {
        // Offer purchasing of a special offer
        SomeStoreUI::ShowSpecialOffer();
    }
}

// Process the result of waiting for A/B test configuration
void SomeLogicClass::OnConfigReceive(EDTDRemoteConfigReceiveResult result) {
    // It is not used in current example
}

// Prepare the app UI for changing the remote configuration
void SomeLogicClass::OnPrepareToChange() {
    // Display the download progress indicator
    SomeUI::ShowLoadingIndicator(WAIT_GROUP_TIME);
}

// Apply the values of the assigned group
void SomeLogicClass::OnConfigChange(EDTDRemoteConfigChangeResult result, const FString&#x26; error) {
    // Hide the download progress indicator
    SomeUI::HideLoadingIndicator();
    switch (result)
    {
    case EDTDRemoteConfigChangeResult::Success:
    	// Apply new values
	UDTDRemoteConfigBPLibrary::ApplyConfig();
	break;
	
    case EDTDRemoteConfigChangeResult::Failure:
	// Error processing
	UE_LOG(LogTemp, Warning, TEXT("DTDRemoteConfigError: %s"), *error);
	break;

    default:
	break;
    }
}
</code></pre>

{% endtab %}

{% tab title="Godot" %}

```gdscript
# Control timer
var timer = Timer.new()
# Timer constant
var waitGroupConst = 10.0
var showStore = "showStore"

func loadDefaults():
	var appDefaults = GDDTDRemoteConfigDefaults.new()
	# Value key
	appDefaults.AddBoolValue(showStore, false)
	DTDRemoteConfig.SetDefaults(appDefaults)
	
func getBool(key: String) -> bool: 
	return DTDRemoteConfig.GetRemoteConfigValue(key).GetBoolValue()

# Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
func startTutorial():
    # Send a trigger event that indicates tutorial start
	DTDAnalytics.Tutorial(-1)

func nextTutorialStep(currentStep: int):
	DTDAnalytics.Tutorial(currentStep)
	if (getBool(showStore) && currentStep == 5):
		#Offer purchasing of a special offer
		pass
	
func _ready():
    # Set the maximum time of waiting for an A/B test group
	DTDRemoteConfig.SetGroupDefinitionWaiting(waitGroupConst)
	# Set default values
	loadDefaults()
	#Initialize the SDK for working with A/B testing
	DTDAnalytics.InitializeWithAbTest("App ID",
	onRemoteConfigChange,
	onRemoteConfigPrepareToChange,
	onRemoteConfigReceive)
	DTDAnalytics.SetLogLevel(GDDTDLogLevel.Debug)
	
func onRemoteConfigChange(result: GDDTDRemoteConfigChangeResult.ChangeResult, error: String):
	match result:
		GDDTDRemoteConfigChangeResult.Success:
			# Apply new values
			DTDRemoteConfig.ApplyConfig()
		GDDTDRemoteConfigChangeResult.Failure:
			# Error processing
			print(error)
			
	timer.stop()
	hideActivityIndicator()
	
# Prepare the app UI for changing the remote configuration
func onRemoteConfigPrepareToChange():
    # Display the download progress indicator
	showActivityIndicator()
	# Add a timer that will forcibly remove the download progress indicator
	timer.connect("timeout", hideActivityIndicator)
	timer.one_shot = true
	timer.wait_time = waitGroupConstInMilliseconds
	add_child(timer)
	timer.start()

# Process the result of waiting for A/B test configuration
func onRemoteConfigReceive(result: GDDTDRemoteConfigReceiveResult.ReceiveResult):
	#It is not used in current example
	pass
```

{% endtab %}
{% endtabs %}

## Example №2

### **Hypothesis**

Our analytics data show that N users installed the app more than half a year ago but still did not make a purchase.  We hypothesize that if we offer a huge discount then we can get additional income.

### Test criteria

The criteria for test inclusion is the app install date and the “payer” status.

![](https://lh3.googleusercontent.com/Rse4-M84G4z055toJozKpEYEZlNDzgdhM6ElQS0MyZlnCFjENYA1z3KjiO7eP1lwnu9Shs_ZikqV_Hunc6u4Hjrn_3UFLWrQlxUw770M7Gj72Z4NRYoR3aJmHut-NYXjumY85tz9q8hR5KBsTpikzDI)

### **Groups**

Control group: current version with usual prices&#x20;

Group А: it has a discount badge on the main page. After clicking on the badge, a purchase window pops up

![](https://lh3.googleusercontent.com/QB-I7KW8Bb9b8qobsmSIFGvzrpPSFXRJSGQsxfgO8O9ybKWVlACnyKEeJnH1rDrZQVyCCsogTQF-59EvSDIx7RGxoHySTdWOoFKN4gITAvlr58T5Se7rYZZofK0D7I-EuNP-1w6nxYcP_xhOTg5Z0rI)

### **Implementation**

{% hint style="danger" %}
**This integration manual is only for SDK versions below 2.6.0 / Unity 3.10.0.**&#x20;

If you are using SDK 2.6.0 / Unity 3.10.0 and higher, please refer to the [updated integration manual](/integration/integration-of-sdk-v2/remote-configuration/rc-integration).
{% endhint %}

{% tabs %}
{% tab title="iOS+macOS (Swift)" %}

```swift
 // Timer constants
struct Constants {
    static let waitConfigConst = 10.0
    static let waitGroupConst = 15.0
}

// Value keys
enum ValueKey: String {
  case maximumDiscount
}

class AppConfig {
    static func loadDefaults() {
        let appDefaults: [String: Any] = [
            ValueKey.maximumDiscount.rawValue: "false"
        ]
        // Set default values
        DTDRemoteConfig.defaults = appDefaults
    }
    
    static func bool(forKey key: ValueKey) -> Bool {
        return DTDRemoteConfig.config[key.rawValue].boolValue
    }
}

class AppLogic {
    override func viewDidLoad() {
        super.viewDidLoad()
        // Display the launch screen
        showLaunchScreen()
    }
    
    // Launching the main logic of the application
    func startAppForReal() {
        // Update app UI
        updateAppUI()
        // Hide launch screen
        hideLaunchScreen()
    }
    
    func updateAppUI() {
        if bool(forKey: .maximumDiscount) {
            // Display a discount badge clicking on which invokes a purchase window popup
        }
    }
}

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication,
        willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Set the maximum time of waiting for the A/B test configuration
        DTDRemoteConfig.remoteConfigWaiting = Constants.waitConfigConst
        // Set the maximum time of waiting for an A/B test group
        DTDRemoteConfig.groupDefinitionWaiting = Constants.waitGroupConst
        // Set default values
        AppConfig.loadDefaults()
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.initializeWithAbTest(applicationKey: "appKey",
                                          configuration: config,
                                          abConfigListener: self)
    }
}

extension AppLogic: DTDRemoteConfigListener {
    // Process the result of waiting for A/B test configuration
    func onReceived(result: DTDRemoteConfigReceiveResult) {
        // If the attempt fails, launch the main logic of the application
        if result == .failure {
            DispatchQueue.main.async { [weak self] in
                self?.startAppForReal()
            }
        }
    }

    // Prepare the app UI for changing the remote configuration
    func onPrepareToChange() {
        // It is not used in current example
    }
    
    // Apply the values of the assigned group
    func onChanged(result: DTDRemoteConfigChangeResult, error: Error?) {
        defer {
            // Launch the main logic of the application
            DispatchQueue.main.async { [weak self] in
              self?.startAppForReal()
            }
        }

        switch result {
        case .success:
            // Apply new values
            DTDRemoteConfig.applyConfig()

        case .failure:
            // Error processing
            if let error = error {
                print(error.localizedDescription)
            }

        @unknown default:
            break
        }
    }
}
```

**Note**

When receiving the configuration, the SDK always calls the `onReceived(result: DTDRemoteConfigReceiveResult)` method, and when enrolling in a test - the `onPrepareToChange()` and `onChanged(result: DTDRemoteConfigChangeResult, error: Error?)` method. However, you can take some additional precocious measures. You can add a timer as in the following example:

```java
func showLaunchScreen() {
    // Add a timer that will forcibly launch the main logic of the application
    timer = Timer.scheduledTimer(withTimeInterval: waitConfigConst + waitGroupConst, 
                                          repeats: false) { [weak self] _ in
        self?.startAppForReal()
    }
    showActivityIndicator()
}
```

{% endtab %}

{% tab title="iOS+macOS (Objective-C)" %}

<pre class="language-objectivec"><code class="lang-objectivec"><strong>// Constants .h + .m
</strong>@interface Constants : NSObject
// Timer constants
extern double const waitConfigConst;
extern double const waitGroupConst;
// Value keys
extern NSString * const maximumDiscount;
@end

@implementation Constants
double const waitConfigConst = 10.0;
double const waitGroupConst = 15.0;
NSString * const maximumDiscount = @"maximumDiscount";
@end

// AppConfig .h + .m
@interface AppConfig: NSObject
+(void) loadDefaults;
+(BOOL) getBoolForKey:(NSString *) key;
@end

@implementation AppConfig
+(void)loadDefaults {
    NSDictionary *appDefaults = @{
        maximumDiscount: @NO
    };

    // Set default values
    DTDRemoteConfig.defaults = appDefaults;
}

+(BOOL) getBoolForKey:(NSString *) key {
    return DTDRemoteConfig.config[key].boolValue;
}
@end

// AppLogic .h + .m
@interface AppLogic : UIViewController &#x3C;DTDRemoteConfigListener>
@end

@implementation AppLogic
- (void)viewDidLoad {
    [super viewDidLoad];
    // Display the launch screen
    [self showLaunchScreen];
}

// Launching the main logic of the application
- (void) startAppForReal {
    // Update app UI
    [self updateAppUI];
    // Hide launch screen
    [self hideLaunchScreen];
}

- (void) updateAppUI {
    if ([AppConfig getBoolForKey:maximumDiscount]) {
        // Display a discount badge clicking on which invokes a purchase window popup
    }
}

// Process the result of waiting for A/B test configuration
- (void)onReceivedResult:(enum DTDRemoteConfigReceiveResult)result {
    // If the attempt fails, launch the main logic of the application
    if (result == DTDRemoteConfigReceiveResultFailure) {
        __weak AppLogic *weakSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [weakSelf startAppForReal];
        });
    }
}

// Prepare the app UI for changing the remote configuration
- (void)onPrepareToChange {
    // It is not used in current example
}

// Apply the values of the assigned group
- (void)onChangedResult:(enum DTDRemoteConfigChangeResult)result error:(NSError *)error {
    switch (result) {
        case DTDRemoteConfigChangeResultSuccess:
            // Apply new values
            [DTDRemoteConfig applyConfig];
            break;

        case DTDRemoteConfigChangeResultFailure:
            // Error processing
            if (error) {
                NSLog(@"DTDRemoteConfigError: %@", error.localizedDescription);
            }

        default:
            break;
    }

    // Launch the main logic of the application
    __weak AppLogic *weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        [weakSelf startAppForReal];
    });
}

- (void)showLaunchScreen {
    // Display the launch screen
}

- (void)hideLaunchScreen {
    // Hide launch screen
}
@end

// AppDelegate .h + .m
@interface AppDelegate ()
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    AppLogic *appLogicController = [[UIStoryboard storyboardWithName:@"Main" bundle:nil] instantiateViewControllerWithIdentifier:@"AppLogic"];
    UINavigationController *navController = [[UINavigationController alloc]initWithRootViewController:appLogicController];
    self.window.rootViewController = navController;
    [self.window makeKeyAndVisible];

    // Config timeout, optional
    DTDRemoteConfig.remoteConfigWaiting = waitConfigConst;
    // Group timeout, optional
    DTDRemoteConfig.groupDefinitionWaiting = waitGroupConst;
    // Implementation defaults params
    [AppConfig loadDefaults];
    [DTDAnalytics applicationKey:appKey abConfigListener:appLogicController];

    return YES;
}
@end
</code></pre>

**Note**

When receiving the configuration, the SDK always calls the `onReceivedResult` method, and when enrolling in a test - the `onPrepareToChange` and `onChangedResult` method. However, you can take some additional precocious measures. You can add a timer as in the following example:

```objectivec
-(void) showLaunchScreen {
    // Add a timer that will forcibly launch the main logic of the application
    self.timer = [NSTimer scheduledTimerWithTimeInterval:waitConfigConst + waitGroupConst
                                                 repeats:false 
                                                   block:^(NSTimer *timer){
        [weakSelf startAppForReal];
    }];
    showActivityIndicator()
}
```

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
class Constants {
    // Timer constants
    companion object {
        const val waitConfigConst = 10.0
        const val waitConfigConstInMilliseconds = 10000L
        const val waitGroupConst = 15.0
        const val waitGroupConstInMilliseconds = 15000L
    }
}

// Value keys
enum class ValueKey(val value: String) {
    MaximumDiscount("maximumDiscount")
}

class AppConfig {
    companion object {
        fun loadDefaults() {
            val appDefaults = mapOf<String, Any>(ValueKey.MaximumDiscount.value to "false")
            // Set default values
            DTDRemoteConfig.defaults = appDefaults
        }

        fun bool(key: ValueKey): Boolean {
            return DTDRemoteConfig.config[key.value].booleanValue
        }
    }
}

class AppLogic {
    fun viewDidLoad() {
        // Display the launch screen
        showLaunchScreen()
    }

    // Launching the main logic of the application
    fun startAppForReal() {
        // Update app UI
        updateAppUI()
        // Hide launch screen
        hideLaunchScreen()
    }

    fun updateAppUI() {
        if (AppConfig.bool(ValueKey.MaximumDiscount)) {
            // Display a discount badge clicking on which invokes a purchase window popup
        }
    }
}

class MainActivity : AppCompatActivity(), DTDRemoteConfigListener {
    //activityIndicator control timer
    var timer: Timer? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState, persistentState)
        setContentView(R.layout.activity_main)

        // Set the maximum time of waiting for the A/B test configuration
        DTDRemoteConfig.remoteConfigWaiting = Constants.waitConfigConst
        // Set the maximum time of waiting for an A/B test group
        DTDRemoteConfig.groupDefinitionWaiting = Constants.waitGroupConst
        // Set default values
        AppConfig.loadDefaults()
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.initializeWithAbTest(
            appKey = "appKey",
            context = this,
            abConfigListener = this
        )
    }

    // Process the result of waiting for A/B test configuration
    override fun onReceived(result: DTDRemoteConfigReceiveResult) {
        // If the attempt fails, launch the main logic of the application
        if (result == DTDRemoteConfigReceiveResult.Failure) {
            runOnUiThread {
                startAppForReal()
            }
        }
    }


    // Prepare the app UI for changing the remote configuration
    override fun onPrepareToChange() {
        // It is not used in current example
    }

    override fun onChanged(result: DTDRemoteConfigChangeResult, ex: Exception?) {
        when (result) {
            DTDRemoteConfigChangeResult.Success -> {
                // Apply new values
                DTDRemoteConfig.applyConfig()
            }
            DTDRemoteConfigChangeResult.Failure -> {
                // Error processing
                ex?.let { Log.e("TAG", ex.toString()) }
            }
        }

        runOnUiThread {
            startAppForReal()
        }
    }
}
```

**Note**

When receiving the configuration, the SDK always calls the `onReceivedResult` method, and when enrolling in a test - the `onPrepareToChange` and `onChangedResult` method. However, you can take some additional precocious measures. You can add a timer as in the following example:

```kotlin
fun showLaunchScreen() {
    // Add a timer that will forcibly launch the main logic of the application
    tiemr = Timer("ActivityIndicator").schedule(
        Constants.waitConfigConstInMilliseconds +
                Constants.waitGroupConstInMilliseconds
    ) {
        runOnUiThread {
            startAppForReal()
        }
    }
    showActivityIndicator()
}
```

{% endtab %}

{% tab title="Android (Java)" %}

```java
class Constants {
    // Timer constants
    final static double waitGroupConst = 10.0;
    final static long waitGroupConstInMilliseconds = 10000L;

    final static double waitConfigConst = 15.0;
    final static long waitConfigConstInMilliseconds = 15000L;
}

enum ValueKey {
    MaximumDiscount("maximumDiscount");

    private final String stringValue;

    ValueKey(String toString) {
        stringValue = toString;
    }

    @Override
    public String toString() {
        return stringValue;
    }
}

class AppConfig {
    static void loadDefaults() {
        HashMap<String, Object> map = new HashMap<>();
        map.put(ValueKey.MaximumDiscount.toString(), "false");
        DTDRemoteConfig.INSTANCE.setDefaults(map);
    }

    static Boolean bool(ValueKey key) {
        return DTDRemoteConfig.INSTANCE.getConfig().get(key.toString()).getBooleanValue();
    }
}

class AppLogic {
    static void viewDidLoad() {
        // Display the launch screen
        showLaunchScreen();
    }

    // Launching the main logic of the application
    static void startAppForReal() {
        // Update app UI
        updateAppUI();
        // Hide launch screen
        hideLaunchScreen();
    }

    static void updateAppUI() {
        if (AppConfig.bool(ValueKey.MaximumDiscount)) {
            // Display a discount badge clicking on which invokes a purchase window popup
        }
    }
}

class MainActivity extends AppCompatActivity implements DTDRemoteConfigListener {
    //activityIndicator control timer
    Timer timer = null;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Set the maximum time of waiting for the A/B test configuration
        DTDRemoteConfig.INSTANCE.setRemoteConfigWaiting(Constants.waitConfigConst);
        // Set the maximum time of waiting for an A/B test group
        DTDRemoteConfig.INSTANCE.setGroupDefinitionWaiting(Constants.waitGroupConst);
        // Set default values
        AppConfig.loadDefaults();
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.INSTANCE.initializeWithAbTest("appKey", this, this);
    }

    // Process the result of waiting for A/B test configuration
    @Override
    public void onReceived(@NonNull DTDRemoteConfigReceiveResult result) {
        // If the attempt fails, launch the main logic of the application
        if (result == DTDRemoteConfigReceiveResult.Failure) {
            runOnUiThread(AppLogic::startAppForReal);
        }
    }

    // Prepare the app UI for changing the remote configuration
    @Override
    public void onPrepareToChange() {
        // It is not used in current example
    }

    @Override
    public void onChanged(@NonNull DTDRemoteConfigChangeResult result, @Nullable Exception ex) {
        if (result == DTDRemoteConfigChangeResult.Success) {
            // Apply new values
            DTDRemoteConfig.INSTANCE.applyConfig();
        }

        if (result == DTDRemoteConfigChangeResult.Failure) {
            // Error processing
            if (ex != null) {
                Log.e("TAG", ex.toString());
            }
        }

        if (timer != null) {
            timer.cancel();
            timer = null;
        }

        runOnUiThread(AppLogic::startAppForReal);
    }
}
```

**Note**

When receiving the configuration, the SDK always calls the `onReceivedResult` method, and when enrolling in a test - the `onPrepareToChange` and `onChangedResult` method. However, you can take some additional precocious measures. You can add a timer as in the following example:

```java
    void showLaunchScreen() {
        // Add a timer that will forcibly launch the main logic of the application
        timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                runOnUiThread(AppLogic::startAppForReal);
            }
        }, Constants.waitConfigConstInMilliseconds +
                Constants.waitGroupConstInMilliseconds);

        showActivityIndicator();
    }
```

{% endtab %}

{% tab title="Unity" %}

```csharp
 // Timer constants
public static class Constants
{
    public static float WAIT_GROUP_TIME = 10.0f;
    public static float WAIT_CONFIG_TIME = 15.0f;
}

// Value keys
public enum ValueKey
{
    maximumDiscount
}

public class AppConfig
{
    public void LoadDefaults()
    {
        var appDefaults = new Dictionary<string, object>
        {
            {ValueKey.maximumDiscount.ToString(), false}
        };
        // Set default values
        DTDRemoteConfig.Defaults = appDefaults;
    }
    
    public bool GetBool(ValueKey key) => DTDRemoteConfig.Config[key.ToString()].BoolValue();
}

public class AppLogic : MonoBehaviour, IDTDRemoteConfigListener
{
    private readonly AppConfig _appConfig = new AppConfig();
    private const string APP_KEY = "appKey";
    private SimpleUI _simpleUI;

    private void Start()
    {
        DontDestroyOnLoad(this);
        _simpleUI = FindObjectOfType<SimpleUI>();
        if (_simpleUI == null) throw new NullReferenceException("UIManager not found.");
        // Set the maximum time of waiting for an A/B test group
        DTDRemoteConfig.GroupDefinitionWaiting = Constants.WAIT_GROUP_TIME;
        // Set the maximum time of waiting for the A/B test configuration
        DTDRemoteConfig.RemoteConfigWaiting = Constants.WAIT_CONFIG_TIME;
        // Set default values
        _appConfig.LoadDefaults();
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.InitializeWithAbTests(
            appKey: APP_KEY,
            configListener: this);

        // Show the loading indicator with timer.
        _simpleUI.ShowLoadingIndicator(Constants.WAIT_GROUP_TIME + Constants.WAIT_CONFIG_TIME);
    }

    public void UpdateAppUI()
    {
        if (_appConfig.GetBool(ValueKey.maximumDiscount))
        {
            // Display a discount badge clicking on which invokes a purchase window popup
        }
    }

    // Process the result of waiting for A/B test configuration.    
    public void OnReceived(DTDRemoteConfigReceiveResult result)
    {
        // If the attempt fails, hide the loading indicator and stop timer.
        if (result == DTDRemoteConfigReceiveResult.Failure)
        {
            _simpleUI.HideLoadingIndicator();
        }
    }

    // Prepare the app UI for changing the remote configuration    
    public void OnPrepareToChange()
    {
        // It is not used in current example
    }
    
    // Apply the values of the assigned group
    public void OnChanged(DTDRemoteConfigChangeResult result, string exceptionText = null)
    {
        // Hide the loading indicator and stop timer.
        _simpleUI.HideLoadingIndicator();
        switch (result)
        {
            case DTDRemoteConfigChangeResult.Failure:
                // Error processing
                if (exceptionText != null) Debug.LogError(exceptionText);
                break;
            case DTDRemoteConfigChangeResult.Success:
                // Apply new values
                DTDRemoteConfig.ApplyConfig();
                break;
        }
        
        UpdateAppUI();
    }
}
```

**Note**

When receiving the configuration, the SDK always calls the `OnReceived(DTDRemoteConfigReceiveResult result)` method, and when enrolling in a test - the `OnPrepareToChange()` and `OnChanged(DTDRemoteConfigChangeResult result, string exceptionText = null)` method. However, you can take some additional precocious measures. You can add a timer as in the following example:

```csharp
public class SimpleUI : MonoBehaviour
{
    [SerializeField] private Canvas loadingIndicator;

    private Coroutine _timer;

    private IEnumerator HideAfterTimeout(float timeout, Canvas target)
    {
        yield return new WaitForSeconds(timeout);
        target.gameObject.SetActive(false);
    }

    public void ShowLoadingIndicator(float? timeout = null)
    {
        loadingIndicator.gameObject.SetActive(true);
        if (timeout != null)
            _timer = StartCoroutine(HideAfterTimeout(timeout.Value, loadingIndicator));
    }
    
    public void HideLoadingIndicator()
    {
        loadingIndicator.gameObject.SetActive(false);
        if (_timer != null) StopCoroutine(_timer);
    }
}
```

{% endtab %}

{% tab title=".NET + UWP" %}

<pre class="language-csharp"><code class="lang-csharp">using DevToDev.Analytics;
using System.Collections.Generic;
using System.Diagnostics;

// Timer constants
static class Constants
{
    public const float WaitConfigConst = 10.0f;
    public const float WaitGroupConst = 15.0f;
}

// Value keys
static class ValueKeys
{
    public const string MaximumDiscount = "maximumDiscount";
}

static class AppConfg
{
    public static void LoadDafaults()
    {
        // Set default values
        DTDRemoteConfig.Defaults = new Dictionary&#x3C;string, object>
        {
            [ValueKeys.MaximumDiscount] = false
        };
    }

    public static bool GetBoolValue(string key)
    {
        return DTDRemoteConfig.Config[key].BoolValue;
    }
}

class Application : IDTDRemoteConfigListener
{
    public void Run()
    {
        // Set the maximum time of waiting for an A/B test group
        DTDRemoteConfig.RemoteConfigWaiting = Constants.WaitConfigConst;
        // Set the maximum time of waiting for the A/B test configuration
        DTDRemoteConfig.GroupDefinitionWaiting = Constants.WaitGroupConst;
        // Set default values
        AppConfg.LoadDafaults();
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.InitializeWithAbTest("appKey", this);
        // Show launch screen (method do a job in UI thread)
        UI.ShowLaunchScreen();
    }

    public void ShowMainScene()
    {
        // Show main screen (also hide launch screen) (method do a job in UI thread)
        UI.ShowMainScreen();
        if (AppConfg.GetBoolValue(ValueKeys.MaximumDiscount))
        {
            // Display a discount badge clicking on which invokes a purchase window popup
            // (method do a job in UI thread)
            UI.ShowMaximumDiscount();
        }
    }

<strong>    // Apply the values of the assigned group
</strong>    public void OnChanged(DTDRemoteConfigChangeResult result, string error)
    {
        Debug.WriteLine($"[App-ABTests] OnChanged({result}, {error})");
        switch (result)
        {
            case DTDRemoteConfigChangeResult.Failure:
                // Error processing
                break;
            case DTDRemoteConfigChangeResult.Success:
                // Apply new values
                DTDRemoteConfig.ApplyConfig();
                break;
            default:
                break;
        }

        // Show main screen (also hide launch screen)
        ShowMainScene();
    }

<strong>    // Prepare the app UI for changing the remote configuration   
</strong>    public void OnPrepareToChange()
    {
        // It is not used in current example
        Debug.WriteLine($"[App-ABTests] OnPrepareToChange()");
    }

<strong>    // Process the result of waiting for A/B test configuration.   
</strong>    public void OnReceived(DTDRemoteConfigReceiveResult result)
    {
        Debug.WriteLine($"[App-ABTests] OnReceived({result})");
        // If the attempt fails, show main screen (also hide launch screen)
        if (result == DTDRemoteConfigReceiveResult.Failure)
        {
            ShowMainScene();
        }
    }
}
</code></pre>

**Note**

When receiving the configuration, the SDK always calls the `OnReceived(DTDRemoteConfigReceiveResult result)` method, and when enrolling in a test - the `OnPrepareToChange()` and `OnChanged(DTDRemoteConfigChangeResult result, string error)`. However, you can take some additional precocious measures. You can add a timer as in the following example:

```csharp
public void showLaunchScreen() {
    // Add a timer that will forcibly launch the main logic of the application
    TimerUtil.CallWithDelay(Constants.WaitConfigConst + Constants.WaitGroupConst, () =>
    {
        UI.StartAppForReal();
    });
    UI.ShowActivityIndicator();
}
```

{% endtab %}

{% tab title="Web" %}

```javascript
devtodev.remoteConfig.defaults = {
   maximumDiscount: false,
}
devtodev.remoteConfig.groupDefinitionWaiting = 10
devtodev.remoteConfig.remoteConfigWaiting = 15
devtodev.initializeWithAbTest(
    appKey, 
    {
        userId: userId,
        logLevel: logLevel,
        trackingAvailability: trackingAvailability,
    },
    {
        // Process the result of waiting for A/B test configuration
        onReceived: function(result) {
            // It is not used in current example
        },
        // Prepare the app UI for changing the remote configuration
        onPrepareToChange: function() {
            // Display the progress indicator
            ui.showSpinner()
        },
        // Apply the values of the assigned group
        onChanged: function(result, error) {
            ui.hideSpinner()
            switch (result) {
              case DTDRemoteConfigChangeResult.Failure:
                  // Error processing
                  console.error(error);
                  break;
              case DTDRemoteConfigChangeResult.Success:
                  // Apply new values
                  devtodev.remoteConfig.applyConfig()
                  break;
            }
            updateAppUI();
        }
    }
)
function updateAppUI() {
    var maximumDiscount = window.devtodev.remoteConfig.config['maximumDiscount'].boolValue
    if (maximumDiscount) {
        // Display a discount badge clicking on which invokes a purchase window popup
    }
}
```

{% endtab %}

{% tab title="Unreal" %}

```cpp
// Timer constants
static float WAIT_CONFIG_TIME = 15.0f;
static float WAIT_GROUP_TIME = 10.0f;

void SomeLogicClass::Start() {
    // Set the maximum time of waiting for an A/B test group
    UDTDRemoteConfigBPLibrary::SetRemoteConfigWaiting(WAIT_CONFIG_TIME);
    // Set the maximum time of waiting for the A/B test configuration
    DTDRemoteConfigBPLibrary::SetGroupDefinitionWaiting(WAIT_GROUP_TIME);

    // Set default values
    FDTDRemoteConfigDefaults Defaults;
    Defaults.BoolDefaults.Add("maximumDiscount", false);
    UDTDRemoteConfigBPLibrary::SetDefaults(Defaults);

    const auto onConfigReceive = new FDTDRemoteConfigReceiveResultDelegate();
    onConfigReceive->BindUObject(this, &SomeLogicClass::OnConfigReceive);

    const auto onPrepareToChange = new FDTDRemoteConfigPrepareToChangeDelegate();
    onPrepareToChange->BindUObject(this, &SomeLogicClass::OnPrepareToChange);

    const auto onConfigChange = new FDTDRemoteConfigChangeResultDelegate();
    onConfigChange->BindUObject(this, &SomeLogicClass::OnConfigChange);

    // Initialize the SDK for working with A/B testing
    UDTDAnalyticsBPLibrary::InitializeWithAbTest("AppKey", *onConfigChange, *onPrepareToChange, *onConfigReceive);

    // Show the loading indicator with timer.
    SomeUI::showLaunchScreen(WAIT_CONFIG_TIME + WAIT_GROUP_TIME);
}

void SomeLogicClass::UpdateAppUI()
{
    if (UDTDRemoteConfigBPLibrary::GetRemoteConfigValue("maximumDiscount").BoolValue)
    {
        // Display a discount badge clicking on which invokes a purchase window popup
    }
}

// Process the result of waiting for A/B test configuration
void SomeLogicClass::OnConfigReceive(EDTDRemoteConfigReceiveResult result) {
    // If the attempt fails, hide the loading indicator and stop timer.
    if (result == EDTDRemoteConfigReceiveResult::Failure)
    {
        SomeUI::HideLoadingIndicator();
    }
}

// Prepare the app UI for changing the remote configuration
void SomeLogicClass::OnPrepareToChange() {
    // It is not used in current example
}

// Apply the values of the assigned group
void SomeLogicClass::OnConfigChange(EDTDRemoteConfigChangeResult result, const FString& error) {
    // Hide the loading indicator and stop timer.
    SomeUI::HideLoadingIndicator();
    switch (result)
    {
    case EDTDRemoteConfigChangeResult::Success:
        // Apply new values
	UDTDRemoteConfigBPLibrary::ApplyConfig();
	break;
	
    case EDTDRemoteConfigChangeResult::Failure:
	// Error processing
	UE_LOG(LogTemp, Warning, TEXT("DTDRemoteConfigError: %s"), *error);
	break;

    default:
    	break;
    }

    UpdateAppUI();
}
```

{% endtab %}

{% tab title="Godot" %}

```gdscript
# Control timer
var timer = Timer.new()
# Timer constants
var waitConfigConst = 10.0
var waitGroupConst = 15.0

var maximumDiscount = "maximumDiscount"

func loadDefaults():
	var appDefaults = GDDTDRemoteConfigDefaults.new()
	# Set default values
	appDefaults.AddBoolValue(maximumDiscount, false)
	DTDRemoteConfig.SetDefaults(appDefaults)
	
func getBool(key: String) -> bool: 
	return DTDRemoteConfig.GetRemoteConfigValue(key).GetBoolValue()
	
func viewDidLoad():
	#Display the launch screen
	showLaunchScreen()
	
#Launching the main logic of the application
func startAppForReal():
# Update app UI
	updateAppUI()
# Hide launch screen
	hideLaunchScreen()
	
func updateAppUI() :
	if getBool(maximumDiscount):
	# Display a discount badge clicking on which invokes a purchase window popup
		pass

func _ready():
		# Set the maximum time of waiting for the A/B test configuration
	DTDRemoteConfig.SetRemoteConfigWaiting(waitConfigConst)
		# Set the maximum time of waiting for an A/B test group
	DTDRemoteConfig.SetGroupDefinitionWaiting(waitGroupConst)
		# Set default values
	loadDefaults()
		# Initialize the SDK for working with A/B testing
	var config = GDDTDAnalyticsConfiguration.new()
	DTDAnalytics.InitializeWithConfigWithAbTest("appKey",
	config,
	onRemoteConfigChange,
	onRemoteConfigPrepareToChange,
	onRemoteConfigReceive)

func onRemoteConfigChange(result: GDDTDRemoteConfigChangeResult.ChangeResult, error: String):
	match result:
		GDDTDRemoteConfigChangeResult.Success:
			# Apply new values
			DTDRemoteConfig.ApplyConfig()
		GDDTDRemoteConfigChangeResult.Failure:
			# Error processing
			print(error)
			
	startAppForReal()

# Prepare the app UI for changing the remote configuration
func onRemoteConfigPrepareToChange():
	# It is not used in current example
	pass

# Process the result of waiting for A/B test configuration
func onRemoteConfigReceive(result: GDDTDRemoteConfigReceiveResult.ReceiveResult):
	if (result == GDDTDRemoteConfigReceiveResult.Failure):
		# If the attempt fails, launch the main logic of the application
		startAppForReal()
		
func showLaunchScreen():
	# Add a timer that will forcibly launch the main logic of the application
	timer.connect("ActivityIndicator", startAppForReal)
	timer.one_shot = true
	timer.wait_time = waitConfigConst + waitGroupConst
	add_child(timer)
	timer.start()
	showActivityIndicator()
```

{% endtab %}
{% endtabs %}

## **Example №3**

### **Hypothesis**

Our analytics data show that only 60% of our users complete the tutorial. We hypothesize that if we make it easier (group A) or reduce the number of seps (group B), we will increase the percentage of tutorial completion.

### Test criteria

New users who have not started the tutorial yet.

![](https://lh6.googleusercontent.com/NpCwocYK8xJOD51WLTavDvAd_oLRO9ngU3MacHVEQjclve2HgVk_NCFcvud9XOwXmq8oiauW-FLlAby6NFQVckroH5usxCytS3Lg_MtCE-2zkf9KiRUCwa4qFoJ4CX59iO-P-iAEvVna6GaXCM9MaxE)

### **Groups**

Control group: current version with usual difficulty and usual number of steps&#x20;

Group А: low difficulty, usual number of steps&#x20;

Group B: usual difficulty, few steps

![](https://lh5.googleusercontent.com/vwZ-8GHvyRwR1JgQ4nuiYIvA8IJw0v9XO0nfbRImJGQrz6cR04rYcaXalEn2InNtitUsrA5su0oEZ-xuHb5zosNXmqv40Rg4YtirKwzjHfEYyOJGkssArP3IaceBKAp8JJZH6WXt4XcswFKbrDzO5Jg)

### **Implementation**

{% hint style="danger" %}
**This integration manual is only for SDK versions below 2.6.0 / Unity 3.10.0.**&#x20;

If you are using SDK 2.6.0 / Unity 3.10.0 and higher, please refer to the [updated integration manual](/integration/integration-of-sdk-v2/remote-configuration/rc-integration).
{% endhint %}

{% tabs %}
{% tab title="iOS+macOS (Swift)" %}

```swift
// Timer constants
struct Constants {
    static let waitConfigConst = 10.0
}

// Value keys
enum ValueKey: String {
    case difficulty
    case stepCount
}

class AppConfig {
    static func loadDefaults() {
        func loadDefaultValues() {
        let appDefaults: [String: Any] = [
            ValueKey.difficulty.rawValue: 3,
            ValueKey.stepCount.rawValue: 10
        ]
        // Set default values
        DTDRemoteConfig.defaults = appDefaults
    }
    
    func integer(forKey key: ValueKey) -> Int {
        DTDRemoteConfig.config[key.rawValue].integerValue
    }
}

class AppLogic {
    // Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
    func startTutorial() {
        // Display the download progress indicator
        showActivityIndicator()
        // Send a trigger event that indicates tutorial start
        DTDAnalytics.tutorial(step: -1)
    }

    // Initiate tutorial completion
    func realStartTutorial() {
        // Hide the download progress indicator
        hideActivityIndicator()
        // Initiate the tutorial by using the remote values (difficulty and stepCount)
        runTutorial(stepCount: integer(forKey: .stepCount), 
                    difficulty: integer(forKey: .difficulty))
    }
}

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication,
        willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Set the maximum time of waiting for the A/B test configuration
        DTDRemoteConfig.remoteConfigWaiting = Constants.waitConfigConst
        // Set default values
        AppConfig.loadDefaults()
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.initializeWithAbTest(applicationKey: "appKey",
                                          configuration: config,
                                          abConfigListener: self)
    }
}

extension AppLogic: DTDRemoteConfigListener {
    // Process the result of waiting for A/B test configuration
    func onReceived(result: DTDRemoteConfigReceiveResult) {
        // It is not used in current example
    }

    // Prepare the app UI for changing the remote configuration
    func onPrepareToChange() {
        // It is not used in current example
    }
    
    // Apply the values of the assigned group
    func onChanged(result: DTDRemoteConfigChangeResult, error: Error?) {
        defer {
            // Initiate tutorial completion
            DispatchQueue.main.async { [weak self] in
                self?.realStartTutorial()
            }
        }

        switch result {
        case .success:
            // Apply new values
            DTDRemoteConfig.applyConfig()

        case .failure:
            // Error processing
            if let error = error {
                print(error.localizedDescription)
            }

        @unknown default:
            break
        }
    }
}
```

{% endtab %}

{% tab title="iOS+macOS  (Objective-C)" %}

```objectivec
// Constants .h + .m
@interface Constants : NSObject
// Timer constants
extern double const waitConfigConst;
// Value keys
extern NSString * const difficulty;
extern NSString * const stepCount;
@end

@implementation Constants
double const waitConfigConst = 10.0;
NSString * const difficulty = @"difficulty";
NSString * const stepCount = @"stepCount";
@end

// AppConfig .h + .m
@interface AppConfig: NSObject
+(void) loadDefaults;
+(NSInteger) getIntegerForKey:(NSString *) key;
@end

@implementation AppConfig
+(void)loadDefaults {
    NSDictionary *appDefaults = @{
        difficulty: @3,
        stepCount: @10
    };

    // Set default values
    DTDRemoteConfig.defaults = appDefaults;
}

+(NSInteger) getIntegerForKey:(NSString *) key {
    return DTDRemoteConfig.config[key].integerValue;
}
@end

// AppLogic .h + .m
@interface AppLogic : UIViewController <DTDRemoteConfigListener>
@end

@implementation AppLogic

- (void)viewDidLoad {
    [super viewDidLoad];
    // Display the launch screen
    [self startTutorial];
}

// Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
- (void) startTutorial {
    // Display the download progress indicator
    [self showActivityIndicator];
    // Send a trigger event that indicates tutorial start
    [DTDAnalytics tutorialStep:-1];
}

// Initiate tutorial completion
- (void) realStartTutorial {
    // Hide the download progress indicator
    [self hideActivityIndicator];
    // Initiate the tutorial by using the remote values (difficulty and stepCount)
    [self runTutorial: [AppConfig getIntegerForKey:stepCount]
       withDifficulty: [AppConfig getIntegerForKey:difficulty]];
}

- (void) runTutorial:(NSInteger) stepCount withDifficulty:(NSInteger) difficulty {
   // Start tutorial
}

// Process the result of waiting for A/B test configuration
- (void)onReceivedResult:(enum DTDRemoteConfigReceiveResult)result {
    // It is not used in current example
}

// Prepare the app UI for changing the remote configuration
- (void)onPrepareToChange {
    // It is not used in current example
}

// Apply the values of the assigned group
- (void)onChangedResult:(enum DTDRemoteConfigChangeResult)result error:(NSError *)error {
    switch (result) {
        case DTDRemoteConfigChangeResultSuccess:
            // Apply new values
            [DTDRemoteConfig applyConfig];
            break;

        case DTDRemoteConfigChangeResultFailure:
            // Error processing
            if (error) {
                NSLog(@"DTDRemoteConfigError: %@", error.localizedDescription);
            }

        default:
            break;
    }

    // Initiate tutorial completion
    __weak AppLogic *weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        [weakSelf realStartTutorial];
    });
}

- (void)showActivityIndicator {
    // Display the launch screen
}

- (void)hideActivityIndicator {
    // Hide launch screen
}
@end

// AppDelegate .h + .m
@interface AppDelegate ()
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    AppLogic *appLogicController = [[UIStoryboard storyboardWithName:@"Main" bundle:nil] instantiateViewControllerWithIdentifier:@"AppLogic"];
    UINavigationController *navController = [[UINavigationController alloc]initWithRootViewController:appLogicController];
    self.window.rootViewController = navController;
    [self.window makeKeyAndVisible];

    // Set the maximum time of waiting for the A/B test configuration
    DTDRemoteConfig.remoteConfigWaiting = waitConfigConst;
    // Implementation defaults params
    [AppConfig loadDefaults];
    [DTDAnalytics applicationKey:appKey abConfigListener:appLogicController];

    return YES;
}
@end
```

{% endtab %}

{% tab title="Android (Kotlin)" %}

```kotlin
class Constants {
    // Timer constants
    companion object {
        const val waitConfigConst = 10.0
        const val waitConfigConstInMilliseconds = 10000L
    }
}

enum class ValueKey(val value: String) {
    Difficulty("difficulty"),
    StepCount("stepCount")
}

class AppConfig {
    companion object {
        fun loadDefaults() {
            val appDefaults = mapOf<String, Any>(
                ValueKey.Difficulty.value to 3,
                ValueKey.StepCount.value to 10
            )
            // Set default values
            DTDRemoteConfig.defaults = appDefaults
        }

        fun integer(key: ValueKey): Int {
            return DTDRemoteConfig.config[key.value].intValue
        }
    }
}

class AppLogic {
    // Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
    fun startTutorial() {
        // Display the download progress indicator
        showActivityIndicator()
        // Send a trigger event that indicates tutorial start
        DTDAnalytics.tutorial(step = -1)
    }


    // Initiate tutorial completion
    fun realStartTutorial() {
        // Hide the download progress indicator
        hideActivityIndicator()
        // Initiate the tutorial by using the remote values (difficulty and stepCount)
        runTutorial(
            stepCount = AppConfig.integer(ValueKey.StepCount),
            difficulty = AppConfig.integer(ValueKey.Difficulty)
        )
    }
}

class MainActivity : AppCompatActivity(), DTDRemoteConfigListener {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState, persistentState)
        setContentView(R.layout.activity_main)

        // Set the maximum time of waiting for the A/B test configuration
        DTDRemoteConfig.remoteConfigWaiting = Constants.waitConfigConst
        // Set default values
        AppConfig.loadDefaults()
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.initializeWithAbTest(
            appKey = "appKey",
            context = this,
            abConfigListener = this
        )
    }

    // Process the result of waiting for A/B test configuration
    override fun onReceived(result: DTDRemoteConfigReceiveResult) {
        // It is not used in current example
    }


    // Prepare the app UI for changing the remote configuration
    override fun onPrepareToChange() {
        // It is not used in current example
    }

    override fun onChanged(result: DTDRemoteConfigChangeResult, ex: Exception?) {
        when (result) {
            DTDRemoteConfigChangeResult.Success -> {
                // Apply new values
                DTDRemoteConfig.applyConfig()
            }
            DTDRemoteConfigChangeResult.Failure -> {
                // Error processing
                ex?.let { Log.e("TAG", ex.toString()) }
            }
        }

        runOnUiThread {
            realStartTutorial()
        }
    }
}
```

{% endtab %}

{% tab title="Android (Java)" %}

```java
class Constants {
    // Timer constants
    final static double waitGroupConst = 10.0;
    final static long waitGroupConstInMilliseconds = 10000L;
}

enum ValueKey {
    Difficulty("difficulty"),
    StepCount("stepCount");

    private final String stringValue;

    ValueKey(String toString) {
        stringValue = toString;
    }

    @Override
    public String toString() {
        return stringValue;
    }
}

class AppConfig {
    static void loadDefaults() {
        HashMap<String, Object> map = new HashMap<>();
        map.put(ValueKey.Difficulty.toString(), 3);
        map.put(ValueKey.StepCount.toString(), 10);
        DTDRemoteConfig.INSTANCE.setDefaults(map);
    }

    static Integer integer(ValueKey key) {
        return DTDRemoteConfig.INSTANCE.getConfig().get(key.toString()).getIntValue();
    }
}

class AppLogic {
    // Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
    static void startTutorial() {
        // Display the download progress indicator
        showActivityIndicator();
        // Send a trigger event that indicates tutorial start
        DTDAnalytics.INSTANCE.tutorial(-1);
    }

    // Initiate tutorial completion
    static void realStartTutorial() {
        // Hide the download progress indicator
        hideActivityIndicator();
        // Initiate the tutorial by using the remote values (difficulty and stepCount)
        runTutorial(
                AppConfig.integer(ValueKey.StepCount),
                AppConfig.integer(ValueKey.Difficulty)
        );
    }
}

class MainActivity extends AppCompatActivity implements DTDRemoteConfigListener {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Set the maximum time of waiting for the A/B test configuration
        DTDRemoteConfig.INSTANCE.setRemoteConfigWaiting(Constants.waitGroupConst);
        // Set default values
        AppConfig.loadDefaults();
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.INSTANCE.initializeWithAbTest("appKey", this, this);
    }

    // Process the result of waiting for A/B test configuration
    @Override
    public void onReceived(@NonNull DTDRemoteConfigReceiveResult result) {
        // It is not used in current example
    }

    // Prepare the app UI for changing the remote configuration
    @Override
    public void onPrepareToChange() {
        // It is not used in current example
    }

    @Override
    public void onChanged(@NonNull DTDRemoteConfigChangeResult result, @Nullable Exception ex) {
        if (result == DTDRemoteConfigChangeResult.Success) {
            // Apply new values
            DTDRemoteConfig.INSTANCE.applyConfig();
        }

        if (result == DTDRemoteConfigChangeResult.Failure) {
            // Error processing
            if (ex != null) {
                Log.e("TAG", ex.toString());
            }
        }

        runOnUiThread(AppLogic::realStartTutorial);
    }
}
```

{% endtab %}

{% tab title="Unity" %}

```csharp
// Timer constants
public static class Constants
{
    public const float WAIT_CONFIG_TIME = 10.0f;
}
    
// Value keys
public enum ValueKey
{
    difficulty,
    stepCount
}

public class AppConfig
{
    public void LoadDefaults()
    {
        var appDefaults = new Dictionary<string, object>
        {
            {ValueKey.difficulty.ToString(), 3},
            {ValueKey.stepCount.ToString(), 10}
        };
        // Set default values
        DTDRemoteConfig.Defaults = appDefaults;
    }
    
    public int GetInt(ValueKey key) => DTDRemoteConfig.Config[key.ToString()].IntValue();
}

public class AppLogic : MonoBehaviour, IDTDRemoteConfigListener
{
    private readonly AppConfig _appConfig = new AppConfig();
    private const string APP_KEY = "appKey";
    private SimpleUI _simpleUI;
    
    private void Start()
    {
        DontDestroyOnLoad(this);
        _simpleUI = FindObjectOfType<SimpleUI>();
        if (_simpleUI == null) throw new NullReferenceException("UIManager not found.");
        // Set the maximum time of waiting for the A/B test configuration
        DTDRemoteConfig.RemoteConfigWaiting = Constants.WAIT_CONFIG_TIME;
        // Set default values
        _appConfig.LoadDefaults();
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.InitializeWithAbTests(
            appKey: APP_KEY,
            analyticsConfiguration: _analyticsConfiguration,
            configListener: this);
    }

    // Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
    public void StartTutorial()
    {
        // Send a trigger event that indicates tutorial start
        DTDAnalytics.Tutorial(-1);
        // Display the download progress indicator
        _simpleUI.ShowLoadingIndicator(Constants.WAIT_CONFIG_TIME);
    }
    
    // Initiate tutorial completion
    private void RealStartTutorial()
    {
        // Hide the download progress indicator
        _simpleUI.HideLoadingIndicator();
        // Initiate the tutorial by using the remote values (difficulty and stepCount)
        RunTutorial(
            stepCount: DTDRemoteConfig.Config[ValueKey.stepCount].IntValue(),
            difficulty: DTDRemoteConfig.Config[ValueKey.difficulty].IntValue()
        );
    }

    // Process the result of waiting for A/B test configuration
    public void OnReceived(DTDRemoteConfigReceiveResult result)
    {
        // It is not used in current example.
    }

    // Prepare the app UI for changing the remote configuration
    public void OnPrepareToChange()
    {
        // It is not used in current example
    }

    // Apply the values of the assigned group
    public void OnChanged(DTDRemoteConfigChangeResult result, string exceptionText = null)
    {
        switch (result)
        {
            case DTDRemoteConfigChangeResult.Failure:
                // Error processing
                if (exceptionText != null) Debug.LogError(exceptionText);
                break;
            case DTDRemoteConfigChangeResult.Success:
                // Apply new values
                DTDRemoteConfig.ApplyConfig();
                break;
        }
        
        // Initiate tutorial completion
        RealStartTutorial();
    }
}
```

{% endtab %}

{% tab title=".Net + UWP" %}

```csharp
using DevToDev.Analytics;
using System.Collections.Generic;
using System.Diagnostics;

// Timer constants
static class Constants
{
    public const float WaitConfigConst = 10.0f;
}

// Value keys
static class ValueKeys
{
    public const string Difficulty = "difficulty";
    public const string StepCount = "stepCount";
}

static class AppConfg
{
    public static void LoadDafaults()
    {
        // Set default values
        DTDRemoteConfig.Defaults = new Dictionary<string, object>
        {
            [ValueKeys.Difficulty] = 3,
            [ValueKeys.StepCount] = 10
        };
    }

    public static int GetIntValue(string key)
    {
        return DTDRemoteConfig.Config[key].Int32Value;
    }
}

class Application : IDTDRemoteConfigListener
{
    public void Run()
    {
        // Set the maximum time of waiting for the A/B test configuration
        DTDRemoteConfig.RemoteConfigWaiting = Constants.WaitConfigConst;
        // Set default values
        AppConfg.LoadDafaults();
        // Initialize the SDK for working with A/B testing
        DTDAnalytics.InitializeWithAbTest("appKey", this);
        // Prepare tutorial scene to show
        PrepareTutorialScene();
    }

    // Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
    public void PrepareTutorialScene()
    {
        // Display the download progress indicator
        UI.ShowLoadingIndicator();
        // Send a trigger event that indicates tutorial start
        DTDAnalytics.Tutorial(-1);
    }

    // Initiate tutorial completion
    public void StartTutorialScene()
    {
        var difficulty = AppConfg.GetIntValue(ValueKeys.Difficulty);
        var stepCount = AppConfg.GetIntValue(ValueKeys.StepCount);
        // Hide the download progress indicator (method do a job in UI thread)
        UI.HideLoadingIndicator();
        // Initiate the tutorial by using the remote values (method do a job in UI thread)
        UI.ShowTutorialScreen(difficulty, stepCount);
    }
    
    // Apply the values of the assigned group
    public void OnChanged(DTDRemoteConfigChangeResult result, string error)
    {
        Debug.WriteLine($"[App-ABTests] OnChanged({result}, {error})");
        switch (result)
        {
            case DTDRemoteConfigChangeResult.Failure:
                // Error processing
                break;
            case DTDRemoteConfigChangeResult.Success:
                // Apply new values
                DTDRemoteConfig.ApplyConfig();
                break;
            default:
                break;
        }

        // Initiate tutorial completion
        StartTutorialScene();
    }

    // Prepare the app UI for changing the remote configuration
    public void OnPrepareToChange()
    {
        Debug.WriteLine($"[App-ABTests] OnPrepareToChange()");
         // It is not used in current example
    }

    
    // Process the result of waiting for A/B test configuration
    public void OnReceived(DTDRemoteConfigReceiveResult result)
    {
        Debug.WriteLine($"[App-ABTests] OnReceived({result})");
         // It is not used in current example
    }
}
```

{% endtab %}

{% tab title="Web" %}

```javascript
devtodev.remoteConfig.defaults = {
   difficulty: 3,
   stepCount: 10
}
devtodev.remoteConfig.remoteConfigWaiting = 10
devtodev.initializeWithAbTest(
    appKey, 
    {
        userId: userId,
        logLevel: logLevel,
        trackingAvailability: trackingAvailability,
    },
    {
        // Process the result of waiting for A/B test configuration
        onReceived: function(result) {
            // It is not used in current example
        },
        // Prepare the app UI for changing the remote configuration
        onPrepareToChange: function() {
        },
        // Apply the values of the assigned group
        onChanged: function(result, error) {
            ui.hideSpinner()
            switch (result) {
              case DTDRemoteConfigChangeResult.Failure:
                  // Error processing
                  console.error(error);
                  break;
              case DTDRemoteConfigChangeResult.Success:
                  // Apply new values
                  devtodev.remoteConfig.applyConfig()
                  var config = window.devtodev.remoteConfig.config // DTDRemoteConfigCollection
                  break;
            }
            realStartTutorial();
        }
    }
)
function startTutorial() {
    devtodev.tutorial(parseInt(-1))
    // Display the progress indicator
    ui.showSpinner()
}
// Initiate tutorial completion
function realStartTutorial() {
    // Hide the download progress indicator
    ui.hideSpinner();
    var stepCount = window.devtodev.remoteConfig.config['stepCount'].intValue
    var difficulty = window.devtodev.remoteConfig.config['difficulty'].intValue
    // Initiate the tutorial by using the remote values (difficulty and stepCount)
    runTutorial(stepCount, difficulty);
}
```

{% endtab %}

{% tab title="Unreal" %}

```cpp
// Timer constant
const float WAIT_CONFIG_TIME = 10.0f;
    
void SomeLogicClass::Start() {
    // Set the maximum time of waiting for the A/B test configuration
    DTDRemoteConfigBPLibrary::SetRemoteConfigWaiting(WAIT_CONFIG_TIME);

    // Set default values
    FDTDRemoteConfigDefaults Defaults;
    Defaults.IntegerDefaults.Add("difficulty", 3);
    Defaults.IntegerDefaults.Add("stepCount", 10);
    UDTDRemoteConfigBPLibrary::SetDefaults(Defaults);

    const auto onConfigReceive = new FDTDRemoteConfigReceiveResultDelegate();
    onConfigReceive->BindUObject(this, &SomeLogicClass::OnConfigReceive);

    const auto onPrepareToChange = new FDTDRemoteConfigPrepareToChangeDelegate();
    onPrepareToChange->BindUObject(this, &SomeLogicClass::OnPrepareToChange);

    const auto onConfigChange = new FDTDRemoteConfigChangeResultDelegate();
    onConfigChange->BindUObject(this, &SomeLogicClass::OnConfigChange);

    // Initialize the SDK for working with A/B testing
    UDTDAnalyticsBPLibrary::InitializeWithAbTest("AppKey", *onConfigChange, *onPrepareToChange, *onConfigReceive);
}

// Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
void SomeLogicClass::StartTutorial()
{
    // Send a trigger event that indicates tutorial start
    UDTDAnalyticsBPLibrary::Tutorial(-1);
    // Display the download progress indicator
    SomeUI::ShowLoadingIndicator(WAIT_CONFIG_TIME);
}

// Initiate tutorial completion
void SomeLogicClass::RealStartTutorial()
{
    // Hide the download progress indicator
    SomeUI::HideLoadingIndicator();
    // Initiate the tutorial by using the remote values (difficulty and stepCount)
    RunTutorial(UDTDRemoteConfigBPLibrary::GetRemoteConfigValue("difficulty").IntegerValue,
		UDTDRemoteConfigBPLibrary::GetRemoteConfigValue("stepCount").IntegerValue);
}


// Process the result of waiting for A/B test configuration
void SomeLogicClass::OnConfigReceive(EDTDRemoteConfigReceiveResult result) {
    // It is not used in current example.
}

// Prepare the app UI for changing the remote configuration
void SomeLogicClass::OnPrepareToChange() {
    // It is not used in current example
}

// Apply the values of the assigned group
void SomeLogicClass::OnConfigChange(EDTDRemoteConfigChangeResult result, const FString& error) {
    switch (result)
    {
    case EDTDRemoteConfigChangeResult::Success:
    	// Apply new values
	UDTDRemoteConfigBPLibrary::ApplyConfig();
	break;
	
    case EDTDRemoteConfigChangeResult::Failure:
	// Error processing
	UE_LOG(LogTemp, Warning, TEXT("DTDRemoteConfigError: %s"), *error);
	break;

    default:
    	break;
    }

    // Initiate tutorial completion
    RealStartTutorial();
}
```

{% endtab %}

{% tab title="Godot" %}

```gdscript
# Timer constant
var waitConfigConst = 10.0

var difficulty = "difficulty"
var stepCount = "stepCount"

func loadDefaults():
	var appDefaults = GDDTDRemoteConfigDefaults.new()
	# Set default values
	appDefaults.AddIntegerValue(difficulty, 3)
	appDefaults.AddIntegerValue(stepCount, 10)
	DTDRemoteConfig.SetDefaults(appDefaults)
	
	
func getInteger(key: String) -> int: 
	return DTDRemoteConfig.GetRemoteConfigValue(key).GetIntValue()

# Tutorial open event (e.g. by clicking the ‘start tutorial’ button)
func startTutorial():
	# Display the download progress indicator
	showActivityIndicator()
	#Send a trigger event that indicates tutorial start
	DTDAnalytics.Tutorial(-1)

#  Initiate tutorial completion
func realStartTutorial():
	# Hide the download progress indicator
	hideActivityIndicator()
	#Initiate the tutorial by using the remote values (difficulty and stepCount)
	var stepCount = getInteger(stepCount)
	var difficulty = getInteger(stepCount)

func _ready():
		# Set the maximum time of waiting for an A/B test group
	DTDRemoteConfig.SetRemoteConfigWaiting(waitConfigConst)
		# Set default values
	loadDefaults()
		# Initialize the SDK for working with A/B testing
	DTDAnalytics.InitializeWithAbTest(appKey",
	onRemoteConfigChange,
	onRemoteConfigPrepareToChange,
	onRemoteConfigReceive)

func onRemoteConfigChange(result: GDDTDRemoteConfigChangeResult.ChangeResult, error: String):
	match result:
		GDDTDRemoteConfigChangeResult.Success:
			# Apply new values
			DTDRemoteConfig.ApplyConfig()
		GDDTDRemoteConfigChangeResult.Failure:
			# Error processing
			print(error)
				
	realStartTutorial()

# Prepare the app UI for changing the remote configuration
func onRemoteConfigPrepareToChange():
	# It is not used in current example
	pass

# Process the result of waiting for A/B test configuration
func onRemoteConfigReceive(result: GDDTDRemoteConfigReceiveResult.ReceiveResult):
	# It is not used in current example
	pass
```

{% endtab %}
{% endtabs %}


# Remote configuration

{% hint style="success" %}
We will greatly appreciate your feedback. Please add **REMOTE CONFIGS** when submitting your request.
{% endhint %}

{% content-ref url="/pages/ILfpUOti7vVOUd2HNTSL" %}
[Remote configuration SDK integration](/integration/integration-of-sdk-v2/remote-configuration/rc-integration)
{% endcontent-ref %}

{% content-ref url="/pages/AGJ0L98GFZ3i3BVVgmyQ" %}
[Working with remote configuration in devtodev](/integration/integration-of-sdk-v2/remote-configuration/rc-interface)
{% endcontent-ref %}


# Remote configuration SDK integration

{% hint style="success" %}
We will greatly appreciate your feedback. Please add **REMOTE CONFIGS** when submitting your request.
{% endhint %}

{% hint style="warning" %}
**Prerequisite:**&#x20;

Currently remote configs are available only for SDK version 2.6.0 (Android & iOS), Unity 3.10.0, Web 3.0 and higher.&#x20;
{% endhint %}

How to configure remote configuration in devtodev interface:&#x20;

{% content-ref url="/pages/AGJ0L98GFZ3i3BVVgmyQ" %}
[Working with remote configuration in devtodev](/integration/integration-of-sdk-v2/remote-configuration/rc-interface)
{% endcontent-ref %}

## What is a remote configuration

**Remote configuration** (**RC** or **remote config**) is a tool that allows you to remotely change the appearance or logic of the app without publishing a new version of the app.

With the help of remote configs you can:

1. Change app behavior for all users or just for a specific audience.&#x20;
2. Conduct A/B tests to compare different configurations on the same audience and find the best performing one.&#x20;

Please note that in order for remote configuration mechanism to work, you will need to prepare all of the possible changes beforehand. This includes the possible changes in app logic and interface.&#x20;

In devtodev, A/B test is considered a specific case of a remote configuration.&#x20;

### Integration plan in short&#x20;

This is a short description of how the RC integration will work.

1. In your app, you will need to declare a set of variables and set **default** values for these variables. We’ll call these variables **Parameters**.
2. Use these parameters in your app according to your realisation of the app logic. Note that parameter value can change and it will affect the app behavior the way it’s determined by your business logic. There is a [special method to get the current parameter value](#dtdremoteconfiglistener.1).
3. Integrate a [listener method](#dtdremoteconfiglistener.1) that will notify you if at least one parameter value has changed. Add a reaction logic to this notification based on our [proposed strategies](#strategies-for-config-application).
4. [Initialize devtodev SDK with a specific method](#remote-config-initialization) to activate the remote configs.

### How remote configs work&#x20;

1. In the devtodev interface you can change parameter values for a specific audience ([Remote Configuration](/integration/integration-of-sdk-v2/remote-configuration/rc-interface)) or create an A/B test ([A/B Testing](/integration/integration-of-sdk-v2/a-b-testing/working-with-a-b-tests-in-the-devtodev)).
2. During initialization, devtodev SDK makes a request to the server and receives a list of parameters and their new values according to the configuration. If you also conduct an A/B test at the same time, the SDK will receive a set of conditions to enter the test, a test group and parameter values.
3. If the SDK receives a new value for at least one of the parameters in current configuration, it will [notify](#using-the-onchanged-method-with-remote-configuration) you. This change can be triggered by a remote config or when the user enters an A/B test.\
   The SDK will give you [a list of parameter changes](#dtdremoteconfigcollection.1) and triggers for each change source.\
   Using the update notification and list of changes, select [one of the strategies to react.](#strategies-for-config-application)
4. When you receive a notification about parameter changes, you can activate ([apply](#applying-the-config)) the values according to your app logic. You will be able to use the updated parameter values until you receive and apply a new configuration. You can check for changes right after devtodev SDK initialization.&#x20;

{% hint style="warning" %}
Attention!\
If the user enters an A/B test, the SDK will mark their events with an A/B test group regardless of whether you have activated the proposed parameter changes.\
We recommend activating and applying these changes as soon as possible!&#x20;
{% endhint %}

### Parameter value priority&#x20;

The parameter always has a defined **default** value. This value can be changed if you set a new value using the remote configuration.

If a user enters an A/B test, they will receive parameter values according to their test group configuration.&#x20;

{% hint style="warning" %}
Parameter values received during an A/B test rewrite the default values and previously set values from the remote config. These test values **cannot be changed** until you finish the experiment (A/B test value has the highest priority).&#x20;
{% endhint %}

The priority order for parameter value source is the following:

1. **Top priority** – A/B test parameter value.
2. Remote config value from devtodev server.
3. Least priority – Default value defined in the application code.

## Integration&#x20;

### Setting up default parameter values&#x20;

Set the variable (parameter) values in the `DTDRemoteConfig` class using the `DTDRemoteConfig.defaults` property.&#x20;

{% hint style="info" %}
The SDK does not change any values in the `defaults` property.
{% endhint %}

After setting `DTDRemoteConfig.defaults`, you will be able to get parameter values using the `DTDRemoteConfig.config` property.&#x20;

{% hint style="warning" %}
Always use the `config` property to get up-to-date parameter value configurations.
{% endhint %}

### Waiting for an A/B test group <a href="#waiting-for-an-a-b-test-group" id="waiting-for-an-a-b-test-group"></a>

When you start an A/B test in devtodev, the SDK checks the conditions to enter the experiment. If the user is suitable for the experiment, the SDK will wait for a test group from devtodev server to enter the experiment.

{% hint style="info" %}
The default wait time `groupDefinitionWaiting` is 10 seconds.
{% endhint %}

You can reduce or extend the wait time value.

For example:&#x20;

```
 DTDRemoteConfig.groupDefinitionWaiting = 2
```

In this case, the SDK will be waiting for a group from the server no longer than two seconds. After that, the experiment will be cancelled and the test configuration will be impossible to activate. During the following SDK initialization (application re-start) the user will be able to enter the experiment again.&#x20;

{% hint style="warning" %}
You can set the `DTDRemoteConfig.groupDefinitionWaiting` value only **before SDK initialization**!
{% endhint %}

### Remote config initialization <a href="#remote-config-initialization" id="remote-config-initialization"></a>

In order to use the remote configs or A/B tests, use the `DTDAnalytics.initializeWithRemoteConfig` to initialize devtodev SDK.&#x20;

#### Migrating from previous SDK versions  <a href="#migrating-from-previous-sdk-versions" id="migrating-from-previous-sdk-versions"></a>

* If you have previously worked with [devtodev A/B tests](/integration/integration-of-sdk-v2/a-b-testing/description-of-a-b-testing-on-the-sdk-side), you will need to change the `DTDAnalytics.initializeWithAbTest` method to `DTDAnalytics.initializeWithRemoteConfig` for SDK initialization. The `initializeWithAbTest` method is not supported in SDK 2.6.0 (Unity 3.10.0) and higher.
* The `DTDRemoteConfigListener` currently implements only one method – `onChanged(update: DTDRemoteConfigUpdate)` with a different signature. You will need to remove the `onReceived(result: DTDRemoteConfigReceiveResult)` and `onPrepareToChange()` methods, since the updated A/B test mechanism does not utilize these methods.&#x20;

Unlike the previous SDK versions, the `onChanged(update: DTDRemoteConfigUpdate)` method will be called only when there is a change in the remote configuration or A/B test.&#x20;

{% hint style="warning" %}
When you migrate to SDK version 2.6.0 (Unity 3.10.0) and higher, during the first SDK initialization the `onChanged(update: DTDRemoteConfigUpdate)` method will be called automatically if the device is participating in an active A/B test.&#x20;

The method will notify you the config with A/B test variables is ready. Since the device is already participating in the test, the latest configuration is already available in `DTDRemoteConfig.config`.&#x20;

**You must call the** `applyConfig` **method to apply and use the test values.**
{% endhint %}

The following SDK launches will not call the `onChanged(update: DTDRemoteConfigUpdate)` method automatically.&#x20;

### Using the onChanged method with remote configuration <a href="#using-the-onchanged-method-with-remote-configuration" id="using-the-onchanged-method-with-remote-configuration"></a>

1. devtodev server sends new or updated parameter configuration to the SDK.
2. The `onChanged()` method is called.&#x20;

### Using the onChanged method with A/B test <a href="#using-the-onchanged-method-with-a-b-test" id="using-the-onchanged-method-with-a-b-test"></a>

1. The SDK found and activated (entered) a suitable experiment.
2. The `onChanged()` method is called.
3. When the experiment is finished and parameter values are different from the experiment configuration, the `onChanged()` method is called.&#x20;

### Applying the config  <a href="#applying-the-config" id="applying-the-config"></a>

To accept and activate the remote configuration, call the `DTDRemoteConfig.applyConfig()` method.&#x20;

When you apply the new configuration, the default, remote config and test group values will intersect according to their priority order. After that, the new configuration values will be available in the `DTDRemoteConfig.config` property.

There are two options to decline a remote config or participation in an A/B test:

1. Call `DTDRemoteConfig.resetConfig()`.\
   After calling this method, the parameter values will be set to **defaults**. You can use the available remote values again with the next SDK initialization.\
   We recommend using this method for testing.
2. Call `DTDRemoteConfig.invalidateActiveConfig()`.\
   After calling this method, the parameter values will be set to **defaults**. It will be impossible to use the remote values until you publish a new remote configuration.

The SDK stores the received configuration until the user deletes the application from the device. Or until you call `resetConfig` or `invalidateActiveConfig` to reset all the active and unapplied configs to **defaults**.

#### Strategies for config application  <a href="#strategies-for-config-application" id="strategies-for-config-application"></a>

The `onChanged(update: DTDRemoteConfigUpdate)` method notifies the developer that the configuration has changed. The `DTDRemoteConfigUpdate` object stores the list of updated keys.

Besides the key-value pairs, you can check how a parameter was updated (remote config or A/B test) using the `DTDRemoteChangeSource`. Knowing the source of change can be useful to select the best strategy to apply the configuration in different cases at the right moment.&#x20;

1. **Activating config during the current session.** In case you need to get the results of parameter configuration change during the same session, call the `DTDRemoteConfig.applyConfig()` method at any convenient time after the `onChanged` was triggered.\
   The `onChanged` is triggered when the devtodev server sends a new or updated config to the device.
2. **A/B testing.** When you are working with A/B tests, we recommend calling the `DTDRemoteConfig.applyConfig()` method as soon as possible because the user will be assigned a test group immediately after the `onChanged` was triggered.
3. **Activating config in a different session.** If you do not have a config that you need to activate during the current session, you can call the `DTDRemoteConfig.applyConfig()` method at any time during the following SDK launches instead of applying config right after `onChanged` is triggered.&#x20;

## External interfaces description <a href="#external-interfaces-description" id="external-interfaces-description"></a>

### DTDRemoteConfig <a href="#dtdremoteconfig.1" id="dtdremoteconfig.1"></a>

The SDK provides threads synchronization when working with this class.&#x20;

| Property                            | Description                                                                                                                                                         |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `groupDefinitionWaiting:Double`     | <p>Wait time for A/B test configuration. </p><p>Default value - 10.0 (measured in seconds).</p>                                                                     |
| `defaults: Map<String, Any>`        | Paramateres and their default values.                                                                                                                               |
| `config: DTDRemoteConfigCollection` | <p>A collection of current parameters and their values for A/B tests.  </p><p>It allows access to the configuration values by using the subscripting syntaxis. </p> |

| Method                     | Description                                                                                                      |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `applyConfig()`            | Applies the remote config or A/B test configuration. After the call, the new config is ready for use in the app. |
| `resetConfig()`            | Resets the config values to defaults until the next SDK initialization.                                          |
| `invalidateActiveConfig()` | Resets the config values to defaults until a new configuration is published.                                     |
| `cacheTestExperiment()`    | A debug method for saving a test experiment after restarting the application.                                    |

### DTDRemoteConfigCollection <a href="#dtdremoteconfigcollection.1" id="dtdremoteconfigcollection.1"></a>

Wrapper for collecting remote parameters. Enables access to configuration values by using subscripting syntax.

#### DTDRemoteConfigValue <a href="#dtdremoteconfigvalue.1" id="dtdremoteconfigvalue.1"></a>

Wrapper for working with remote configuration variables (parameters). It presents a method for data source identification, as well as methods for presenting values in the form of various data types.&#x20;

| Type                              | Description                                                                 |
| --------------------------------- | --------------------------------------------------------------------------- |
| `DTDRemoteConfigSource.Undefined` | The variable could not be found in the default or the remote configuration. |
| `DTDRemoteConfigSource.Defaults`  | The variable is set by default.                                             |
| `DTDRemoteConfigSource.Remote`    | The variable is set by remote config.                                       |
| `DTDRemoteConfigSource.AbTest`    | The variable is set by the test group.                                      |

| Property     | Type    | Description                          |
| ------------ | ------- | ------------------------------------ |
| stringValue  | String? | Gets the value as a optional string. |
| floatValue   | Float   | Gets the value as a Float.           |
| doubleValue  | Double  | Gets the value as a Double.          |
| int32Value   | Int32   | Gets the value as a Int32.           |
| int64Value   | Int32   | Gets the value as a Int32.           |
| integerValue | Int     | Gets the value as a Int.             |
| boolValue    | Bool    | Gets the value as a Bool.            |

#### DTDRemoteConfigListener <a href="#dtdremoteconfiglistener.1" id="dtdremoteconfiglistener.1"></a>

Implements the method that reports on remote configuration and A/B test update.

| Method                                     | Description                                                                                 |
| ------------------------------------------ | ------------------------------------------------------------------------------------------- |
| `onChanged(update: DTDRemoteConfigUpdate)` | Notifies the developer that the configuration has changed with a list of updated variables. |

#### DTDRemoteConfigUpdate <a href="#dtdremoteconfigupdate.1" id="dtdremoteconfigupdate.1"></a>

| Fields                                     | Description                      |
| ------------------------------------------ | -------------------------------- |
| `val keys: List<DTDRemoteConfigChangeKey>` | Contains a list of updated keys. |

#### DTDRemoteConfigChangeKey <a href="#dtdremoteconfigchangekey.1" id="dtdremoteconfigchangekey.1"></a>

The source of the key update.

| Fields                                                                                   | Description                              |
| ---------------------------------------------------------------------------------------- | ---------------------------------------- |
| <p><code>val key: String</code></p><p><code>val source: DTDRemoteConfigSource</code></p> | Contains the key name and update source. |

### List of errors <a href="#list-of-errors" id="list-of-errors"></a>

These messages may be useful for debugging.

| Error message                                                                      | Description                                                                                                                         |
| ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| \[A/B-Test Module] The Server refused to conduct the experiment.                   | devtodev server refused to send the experiment configuration.                                                                       |
| \[A/B-Test Module] Offer from devtodev not received within the allotted N seconds. | The backend offer is returned after wait time is over e.g. due to a bad internet connection.                                        |
| \[A/B-Test Module] Offer from devtodev not received within the allotted N seconds. | The SDK was unable to receive an offer from the backend within N seconds e.g, due to a bad internet connection or network problems. |

## Remote configuration examples <a href="#remote-configuration-examples" id="remote-configuration-examples"></a>

### Example 1 <a href="#example-1" id="example-1"></a>

In this exapmle we set the default values for variables (parameters) `title`, `buttonText`, `tutorial` before devtodev SDK initialization.

After the `onChanged` method is triggered, we call `DTDRemoteConfig.applyConfig()` and apply the received parameter values without any conditions. All inside the `onChanged` method.

You can get the parameter values in any other place in the app, in addition to the `onChanged` method.&#x20;

{% tabs fullWidth="true" %}
{% tab title="Android (Kotlin)" %}

```kotlin
// Initialization of defaults
DTDRemoteConfig.defaults = mapOf(
   "title" to "local title data",
   "buttonText" to "local button data",
   "tutorial" to "local tutorial data"
)
// DTDAnalytics Initialization
DTDAnalytics.initializeWithRemoteConfig(
    "App ID", config, context.applicationContext,
    object : DTDRemoteConfigListener {
        // onChanged implementation
        override fun onChanged(update: DTDRemoteConfigUpdate) {
           // Apply config data
            DTDRemoteConfig.applyConfig()
            // Take data from config
            val titleValue = DTDRemoteConfig.config["title"].stringValue
            val buttonTextValue = DTDRemoteConfig.config["buttonText"].stringValue
            val tutorialValue = DTDRemoteConfig.config["tutorial"].stringValue
            Log.d("[remoteConfig]", "title after applyConfig: $titleValue")
            Log.d("[remoteConfig]", "buttonTextValue after applyConfig: $buttonTextValue")
            Log.d("[remoteConfiga]", "tutorialValue after applyConfig: $tutorialValue")
        }
    }
)
```

{% endtab %}

{% tab title="iOS+macOS (Swift)" %}

```swift
@main
class AppDelegate: UIResponder, UIApplicationDelegate, DTDRemoteConfigListener {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Initialization of defaults
        DTDRemoteConfig.defaults = [
            "title": "local title data",
            "buttonText": "local button data",
            "tutorial": "local tutorial data"
        ]
        // DTDAnalytics Initialization
        DTDAnalytics.initializeWithRemoteConfig(applicationKey: "App ID", abConfigListener: self)
        return true
    }

    public func onChanged(update: DTDRemoteConfigUpdate) {
        // Apply config data
        DTDRemoteConfig.applyConfig()
        // Take data from config
        let titleValue = DTDRemoteConfig.config["title"].stringValue
        let buttonTextValue = DTDRemoteConfig.config["buttonText"].stringValue
        let tutorialValue = DTDRemoteConfig.config["tutorial"].stringValue
        print("[remoteConfig] title after applyConfig: \(titleValue)");
        print("[remoteConfig] buttonTextValue after applyConfig: \(buttonTextValue)");
        print("[remoteConfiga] tutorialValue after applyConfig: \(tutorialValue)");
    }
}
```

{% endtab %}

{% tab title="Unity" %}

```csharp
 public class RemoteConfigListenerExample : IDTDRemoteConfigListener
    {
        public void OnChanged(Dictionary<string, DTDRemoteConfigSource> updatedKeys)
        {
            DTDRemoteConfig.ApplyConfig();
            // Take data from config

            var titleValue = DTDRemoteConfig.Config["title"].StringValue();
            var buttonTextValue = DTDRemoteConfig.Config["buttonText"].StringValue();
            var tutorialValue = DTDRemoteConfig.Config["tutorial"].StringValue();
            Debug.Log($"[remoteConfig] title after applyConfig: {titleValue}");
            Debug.Log($"[remoteConfig] buttonTextValue after applyConfig: {buttonTextValue}");
            Debug.Log($"[remoteConfiga] tutorialValue after applyConfig: {tutorialValue}");
        }
    }

    public class RemoteConfigExample
    {
        public void InitializeDevToDev(string appKey, DTDAnalyticsConfiguration analyticsConfiguration)
        {
            DTDRemoteConfig.Defaults = new Dictionary<string, object>
            {
                { "title", "local title data" },
                { "buttonText", "local button data" },
                { "tutorial", "local tutorial data" }
            };
            DTDAnalytics.InitializeWithRemoteConfig(appKey, analyticsConfiguration,
                new RemoteConfigListenerExample());
        }
    }
```

{% endtab %}

{% tab title="Web (TS)" %}

```typescript
import DTDAnalytics, {
  type DTDRemoteConfigListener,
  type DTDRemoteConfigUpdate,
  type DTDRemoteConfigValue,
} from '@dev-2-dev/websdk';

const analytics = new DTDAnalytics();

const remoteConfigListener: DTDRemoteConfigListener = {
  onChanged: (_update: DTDRemoteConfigUpdate) => {
    // Apply config data
    analytics.remoteConfig.applyConfig();
    // Take data from config
    const titleValue = (
      analytics.remoteConfig.config['title'] as DTDRemoteConfigValue
    ).stringValue;
    const buttonTextValue = (
      analytics.remoteConfig.config['buttonText'] as DTDRemoteConfigValue
    ).stringValue;
    const tutorialValue = (
      analytics.remoteConfig.config['tutorial'] as DTDRemoteConfigValue
    ).stringValue;

    console.log('[remoteConfig] title after applyConfig: ', titleValue);
    console.log(
      '[remoteConfig] buttonTextValue after applyConfig: ',
      buttonTextValue
    );
    console.log(
      '[remoteConfig] tutorialValue after applyConfig: ',
      tutorialValue
    );
  },
};

// Initialization of defaults
analytics.remoteConfig.defaults = {
  title: 'local title data',
  buttonText: 'local button data',
  tutorial: 'local tutorial data',
};

analytics.initializeWithRemoteConfig(
  'App ID',
  {
    userId: 'your_user_id',
  },
  remoteConfigListener
);
```

{% endtab %}

{% tab title="Web (JS)" %}

```javascript
import DTDAnalytics from '@dev-2-dev/websdk';

const analytics = new DTDAnalytics();

const remoteConfigListener = {
  onChanged: _update => {
    // Apply config data
    analytics.remoteConfig.applyConfig();
    // Take data from config
    const titleValue = analytics.remoteConfig.config['title'].stringValue;
    const buttonTextValue =
      analytics.remoteConfig.config['buttonText'].stringValue;
    const tutorialValue = analytics.remoteConfig.config['tutorial'].stringValue;

    console.log('[remoteConfig] title after applyConfig: ', titleValue);
    console.log(
      '[remoteConfig] buttonTextValue after applyConfig: ',
      buttonTextValue
    );
    console.log(
      '[remoteConfig] tutorialValue after applyConfig: ',
      tutorialValue
    );
  },
};

// Initialization of defaults
analytics.remoteConfig.defaults = {
  title: 'local title data',
  buttonText: 'local button data',
  tutorial: 'local tutorial data',
};

analytics.initializeWithRemoteConfig(
  'App ID',
  {
    userId: 'your_user_id',
  },
  remoteConfigListener
);
```

{% endtab %}
{% endtabs %}

### Example 2 <a href="#example-2" id="example-2"></a>

In this exapmle we set the default values for variables (parameters) `title`, `buttonText`, `tutorial` before devtodev SDK initialization.

After the `onChanged` method is triggered, we check if there are any keys that were updated for the A/B test.

* If there is such key, the user will immediately enter the A/B test and get the experiment configuration.
* If no key was updated for the A/B test, this means the SDK received only new remote configs without A/B test configuration and you do not need to call `DTDRemoteConfig.applyConfig()` right away.
  * You can apply the remote config at any convenient moment even after the app restarts.

We have added a `DTDRemoteConfig.applyConfig()` call after the SDK initialization. Since the `onChanged` was triggered in the previous app launch, we have the up-to-date remote config values on the device.&#x20;

{% tabs fullWidth="true" %}
{% tab title="Android (Kotlin)" %}

```kotlin
// Initialization of defaults
DTDRemoteConfig.defaults = mapOf(
   "title" to "local title data",
   "buttonText" to "local button data",
   "tutorial" to "local tutorial data"
)
// DTDAnalytics Initialization
DTDAnalytics.initializeWithRemoteConfig(
    "App ID", config, context.applicationContext,
    object : DTDRemoteConfigListener {
        // onChanged implementation
        override fun onChanged(update: DTDRemoteConfigUpdate) {
            // Check if there are a/b test keys
            val abTestKey = update.keys.firstOrNull { it.source == DTDRemoteConfigSource.AbTest }?.key
                if (abTestKey != null) {
                    //Apply config data
                    DTDRemoteConfig.applyConfig()

                    // Take data from config
                    val titleValue = DTDRemoteConfig.config["title"].stringValue
                    val buttonTextValue = DTDRemoteConfig.config["buttonText"].stringValue
                    val tutorialValue = DTDRemoteConfig.config["tutorial"].stringValue
                    Log.d("[remoteConfig]", "title after applyConfig with a/b test: $titleValue")
                    Log.d("[remoteConfig]", "buttonTextValue after applyConfig a/b test: $buttonTextValue")
                    Log.d("[remoteConfig]", "tutorialValue after applyConfig a/b test: $tutorialValue")
                }
            }
        }
    }
)
//Apply config data
DTDRemoteConfig.applyConfig()
// Take data from config
val titleValue = DTDRemoteConfig.config["title"].stringValue
val buttonTextValue = DTDRemoteConfig.config["buttonText"].stringValue
val tutorialValue = DTDRemoteConfig.config["tutorial"].stringValue
Log.d("[remoteConfig]", "title after applyConfig: $titleValue")
Log.d("[remoteConfig]", "buttonTextValue after applyConfig: $buttonTextValue")
Log.d("[remoteConfig]", "tutorialValue after applyConfig: $tutorialValue")

```

{% endtab %}

{% tab title="iOS+macOS (Swift)" %}

```swift
// Initialization of defaults
    DTDRemoteConfig.defaults = [
        "title": "local title data",
        "buttonText": "local button data",
        "tutorial": "local tutorial data"
    ]

    // DTDAnalytics Initialization
    DTDAnalytics.initializeWithRemoteConfig(applicationKey: "App ID", abConfigListener: self)

    // onChanged implementation
    public func onChanged(update: DTDRemoteConfigUpdate) {
        // Check if there are a/b test keys
        if let abTestKey = update.keys.first(where: { $0.source == .abTest })?.key {
            // Apply config data
            DTDRemoteConfig.applyConfig()
            // Take data from config
            let titleValue = DTDRemoteConfig.config["title"].stringValue
            let buttonTextValue = DTDRemoteConfig.config["buttonText"].stringValue
            let tutorialValue = DTDRemoteConfig.config["tutorial"].stringValue
            print("[remoteConfig] title after applyConfig: \(titleValue)");
            print("[remoteConfig] buttonTextValue after applyConfig: \(buttonTextValue)");
            print("[remoteConfiga] tutorialValue after applyConfig: \(tutorialValue)");
        }
    }

    //Apply config data
    DTDRemoteConfig.applyConfig()
    // Take data from config
    let titleValue = DTDRemoteConfig.config["title"].stringValue
    let buttonTextValue = DTDRemoteConfig.config["buttonText"].stringValue
    let tutorialValue = DTDRemoteConfig.config["tutorial"].stringValue
    print("[remoteConfig] title after applyConfig: \(titleValue)");
    print("[remoteConfig] buttonTextValue after applyConfig: \(buttonTextValue)");
    print("[remoteConfiga] tutorialValue after applyConfig: \(tutorialValue)");
```

{% endtab %}

{% tab title="Unity" %}

```csharp
public class RemoteConfigListenerExample : IDTDRemoteConfigListener
{
    public void OnChanged(Dictionary<string, DTDRemoteConfigSource> updatedKeys)
    {
        DTDRemoteConfig.ApplyConfig();
        // Take data from config
        var hasAbTestKey = updatedKeys.Any(x => x.Value == DTDRemoteConfigSource.ABTest);
        if (hasAbTestKey)
        {
            var titleValue = DTDRemoteConfig.Config["title"].StringValue();
            var buttonTextValue = DTDRemoteConfig.Config["buttonText"].StringValue();
            var tutorialValue = DTDRemoteConfig.Config["tutorial"].StringValue();
            Debug.Log($"[remoteConfig] title after applyConfig: {titleValue}");
            Debug.Log($"[remoteConfig] buttonTextValue after applyConfig: {buttonTextValue}");
            Debug.Log($"[remoteConfiga] tutorialValue after applyConfig: {tutorialValue}");
        }
    }
}

public class RemoteConfigExample
{
    public void InitializeDevToDev(string appKey, DTDAnalyticsConfiguration analyticsConfiguration)
    {
        DTDRemoteConfig.Defaults = new Dictionary<string, object>
        {
            { "title", "local title data" },
            { "buttonText", "local button data" },
            { "tutorial", "local tutorial data" }
        };
        DTDAnalytics.InitializeWithRemoteConfig(appKey, analyticsConfiguration,
            new RemoteConfigListenerExample());
    }
}
```

{% endtab %}

{% tab title="Web (TS)" %}

```typescript
import DTDAnalytics, {
  DTDRemoteConfigSource,
  type DTDRemoteConfigListener,
  type DTDRemoteConfigUpdate,
  type DTDRemoteConfigValue,
} from '@dev-2-dev/websdk';

const analytics = new DTDAnalytics();

const remoteConfigListener: DTDRemoteConfigListener = {
  onChanged: (update: DTDRemoteConfigUpdate) => {
    // Check if there are a/b test keys
    const abTestKeys = update.keys.filter(
      key => key.source === DTDRemoteConfigSource.AbTest
    );
    if (abTestKeys.length > 0) {
      // Apply config data
      analytics.remoteConfig.applyConfig();

      // Take data from config
      const titleValue = (
        analytics.remoteConfig.config['title'] as DTDRemoteConfigValue
      ).stringValue;
      const buttonTextValue = (
        analytics.remoteConfig.config['buttonText'] as DTDRemoteConfigValue
      ).stringValue;
      const tutorialValue = (
        analytics.remoteConfig.config['tutorial'] as DTDRemoteConfigValue
      ).stringValue;

      console.log('[remoteConfig] title after applyConfig: ', titleValue);
      console.log(
        '[remoteConfig] buttonTextValue after applyConfig: ',
        buttonTextValue
      );
      console.log(
        '[remoteConfig] tutorialValue after applyConfig: ',
        tutorialValue
      );
    }
  },
};

// Initialization of defaults
analytics.remoteConfig.defaults = {
  title: 'local title data',
  buttonText: 'local button data',
  tutorial: 'local tutorial data',
};

analytics.initializeWithRemoteConfig(
  'App ID',
  {
    userId: 'your_user_id',
  },
  remoteConfigListener
);

analytics.remoteConfig.applyConfig();

// Take data from config
const titleValue = (
  analytics.remoteConfig.config['title'] as DTDRemoteConfigValue
).stringValue;
const buttonTextValue = (
  analytics.remoteConfig.config['buttonText'] as DTDRemoteConfigValue
).stringValue;
const tutorialValue = (
  analytics.remoteConfig.config['tutorial'] as DTDRemoteConfigValue
).stringValue;

console.log('[remoteConfig] title after applyConfig: ', titleValue);
console.log(
  '[remoteConfig] buttonTextValue after applyConfig: ',
  buttonTextValue
);
console.log('[remoteConfig] tutorialValue after applyConfig: ', tutorialValue);
```

{% endtab %}

{% tab title="Web (JS)" %}

```javascript
import DTDAnalytics, { DTDRemoteConfigSource } from '@dev-2-dev/websdk';

const analytics = new DTDAnalytics();

const remoteConfigListener = {
  onChanged: update => {
    // Check if there are a/b test keys
    const abTestKeys = update.keys.filter(
      key => key.source === DTDRemoteConfigSource.AbTest
    );
    if (abTestKeys.length > 0) {
      // Apply config data
      analytics.remoteConfig.applyConfig();

      // Take data from config
      const titleValue = analytics.remoteConfig.config['title'].stringValue;
      const buttonTextValue =
        analytics.remoteConfig.config['buttonText'].stringValue;
      const tutorialValue =
        analytics.remoteConfig.config['tutorial'].stringValue;

      console.log('[remoteConfig] title after applyConfig: ', titleValue);
      console.log(
        '[remoteConfig] buttonTextValue after applyConfig: ',
        buttonTextValue
      );
      console.log(
        '[remoteConfig] tutorialValue after applyConfig: ',
        tutorialValue
      );
    }
  },
};

// Initialization of defaults
analytics.remoteConfig.defaults = {
  title: 'local title data',
  buttonText: 'local button data',
  tutorial: 'local tutorial data',
};

analytics.initializeWithRemoteConfig(
  'App ID',
  {
    userId: 'your_user_id',
  },
  remoteConfigListener
);

analytics.remoteConfig.applyConfig();

// Take data from config
const titleValue = analytics.remoteConfig.config['title'].stringValue;
const buttonTextValue = analytics.remoteConfig.config['buttonText'].stringValue;
const tutorialValue = analytics.remoteConfig.config['tutorial'].stringValue;

console.log('[remoteConfig] title after applyConfig: ', titleValue);
console.log(
  '[remoteConfig] buttonTextValue after applyConfig: ',
  buttonTextValue
);
console.log('[remoteConfig] tutorialValue after applyConfig: ', tutorialValue);
```

{% endtab %}
{% endtabs %}


# Working with remote configuration in devtodev

{% hint style="success" %}
We will greatly appreciate your feedback. Please add **REMOTE CONFIGS** when submitting your request.
{% endhint %}

**Remote Configuration** (**Remote config** or **RC**) allows you to customize the app by sending key-value pairs directly to the user’s device. These configs are defined in the devtodev interface and sent to the SDK immediately or at a scheduled time. Remote configs are stored on the devtodev servers. This allows to trigger changes in the app, without updating app versions or changing app code every time.&#x20;

{% hint style="warning" %}
**Prerequisite:**&#x20;

Currently remote configs are available only for SDK version 2.6.0 (Android & iOS), Unity 3.10.0, Web 3.0 and higher.&#x20;
{% endhint %}

## Preparation

Before using a **remote configuration** in devtodev interface, you need to set variables through the SDK and use the methods to apply new parameters. If the app is offline and will not be able to present the config to the user, they will see default values and the app will continue to function correctly.&#x20;

Integrate remote configs with devtodev SDK:&#x20;

{% content-ref url="/pages/ILfpUOti7vVOUd2HNTSL" %}
[Remote configuration SDK integration](/integration/integration-of-sdk-v2/remote-configuration/rc-integration)
{% endcontent-ref %}

## How to create a remote configuration

In order to create and send a **remote configuration** to user devices, you need to set **parameter** values and define **conditions** to select the audience.&#x20;

{% stepper %}
{% step %}

### [Create conditions](#create-new-condition)

Define the audience and schedule changes
{% endstep %}

{% step %}

### [Create parameters](#create-new-parameter)

Set values and select conditions
{% endstep %}

{% step %}

### Publish changes

The configuration is finalised and sent to user devices.&#x20;

Click `Publish changes` to update the configuration.&#x20;

<figure><img src="/files/AMbRoQvQIqXdwXHiIqlS" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}

### Conditions

**Conditions** define the user group (similar to a segment) that will see the modified interface. You can use basic and custom user properties, as well as previously created segments, to filter the audience.&#x20;

The order of conditions determines their evaluation sequence. The parameter value is assigned based on the first condition that evaluates to true. The condition at the top of the list has the highest priority.&#x20;

The order is important since several conditions may apply to one user. For example, <mark style="background-color:green;">**Parameter A**</mark> is sent to users from the <mark style="background-color:green;">**US that have finished 5 levels**</mark>. <mark style="background-color:red;">**Parameter B**</mark> is for users that have <mark style="background-color:red;">**completed the onboarding**</mark> (onboarding is obligatory). In this case, both parameters can apply to the <mark style="background-color:green;">**US, 5+ levels**</mark> group of users. You can change the order of the Conditions to define the parameter configuration these users will see.&#x20;

<figure><img src="/files/vNenehzh6hlfHjUbp7fO" alt=""><figcaption></figcaption></figure>

You can filter the conditions by selecting `Filter` in the top right corner.&#x20;

<figure><img src="/files/tf5kmY0F2HOtAslDacoq" alt=""><figcaption></figcaption></figure>

#### Create new condition

Click `+Condition` to add a new condition. &#x20;

<figure><img src="/files/Q2HUPmdH63yXy6rBuHzI" alt=""><figcaption></figcaption></figure>

1. Configure your **condition settings**:&#x20;

* **Condition name**
* **Color** – select a color tag. You can use the color tags to organize and filter your conditions.&#x20;
* **Description** – add details about this condition and audience.&#x20;

<figure><img src="/files/Nstx6mzSSQyW7N6tyTAs" alt=""><figcaption></figcaption></figure>

2. Next, define the **Audience conditions** with user properties.&#x20;

<figure><img src="/files/puWwwrTZlpoV2w7xnVB0" alt=""><figcaption></figcaption></figure>

3. Select when the new parameter value will apply to the condition audience:&#x20;

* **Permanent** – the changes will apply instantly.&#x20;
* **Scheduled** – the changed parameter value will apply only during the set time frame.&#x20;

Click `Finish` to complete condition creation.

<figure><img src="/files/bdvA29PpI5iZXnUqCifK" alt=""><figcaption></figcaption></figure>

You can change the condition settings later using the `Edit` button (pencil icon) or simply by clicking on the condition card.

### Parameters&#x20;

**Parameters** represent changes in the app’s UI/UX, a configuration of the interface visible to selected users. They should be pre-implemented in the app and linked to a variable. Parameter value is stored in a key-value format, for example, “button\_color” = “green“. &#x20;

The **Parameters** section shows detailed information about all of your created parameters:&#x20;

* **Parameter** – name, value type and description.
* **Condition** – to what audience does this parameter apply.&#x20;
* **Value** – shows parameter value for each condition.&#x20;
* **Users** – how many users have this configuration.&#x20;
* **Last published** – date when the configuration was published.&#x20;

You can filter the parameters by selecting `Filter` in the top roght corner.

<figure><img src="/files/lTqtBLdogqdxBFVoygdi" alt=""><figcaption></figcaption></figure>

#### Create new parameter

Click `+Parameter` to add a new parameter.&#x20;

<figure><img src="/files/lhJeP1gNGvLbTpFlKVxH" alt=""><figcaption></figcaption></figure>

Configure your **parameter settings**:&#x20;

* **Parameter name (key)** – the name should be unique and correspond to the variable in your app code.&#x20;
* **Type** – value type can be a Number, String, JSON or Boolean.&#x20;
* **Description** –  what does this parameter change in the app.&#x20;
* **Default value** – set the default value for any condition.&#x20;
  * You can use `in-app default` – in this case the parameter will use the default value define in your app code.&#x20;

<figure><img src="/files/EgrtCS1ylPknmI7pzKoc" alt=""><figcaption></figcaption></figure>

Next, configure **Conditional values**.&#x20;

1. Click `+Condition` and select one or multiple conditions from the list.&#x20;
2. Define a parameter value for each condition or use an `in-app deafult`.&#x20;
3. Click `Finish` to complete parameter creation.

If you do not have any created [**Conditions**](#conditions) yet, you can click `Finish` and add a condition later.&#x20;

{% hint style="info" %}
Without conditions, parameter changes will be applied to all users.&#x20;
{% endhint %}

<figure><img src="/files/toNJ9im6lcXxUHiFC0QJ" alt=""><figcaption></figcaption></figure>

You can change the parameter later using the `Edit` button in three dots menu.

#### Folders

For convenience, you can organise parameters in folders. Click `Add Folder`, give it a name and description and click `Create Folder`.&#x20;

<figure><img src="/files/CYH2imRP6CS8FMwGb5im" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/gSBhSlvkStXoXSphvR13" alt=""><figcaption></figcaption></figure>

You can move parameters to different folders.&#x20;

* Click on three dots on the right and click `Move to folder`, select a destination folder and click `Move`. &#x20;
* You can also move parameters in bulk: select the necessary parameters and click `Move to Folder` button in the top right corner.  &#x20;

To edit folder information, delete or ungroup a folder, select a corresponding option in the three dots menu.&#x20;

<figure><img src="/files/6vMWjXbufxEYP7spPKYA" alt=""><figcaption></figcaption></figure>

## Changelog&#x20;

Each configuration of parameters and conditons is stored as a different version.&#x20;

You can compare the versions side by side and, if needed, `Roll back` to an old version of the configuration. Rolling back will create a new version of the config and send changes to user devices.&#x20;

<div><figure><img src="/files/sYLs6RyLTuhr1Iv58Zkc" alt=""><figcaption><p>Parameters changelog</p></figcaption></figure> <figure><img src="/files/Q3tzMz5XJJp2BqZonnwX" alt=""><figcaption><p>Conditions changelog</p></figcaption></figure></div>

## Using remote configuration with A/B tests

We will add an option to send a parameter configuration directly from the A/B test page in the future updates.&#x20;

If a user enters an A/B test, they will receive parameter values according to their test group configuration.&#x20;

{% hint style="warning" %}
Parameter values received during an A/B test rewrite the default values and previously set values from the remote config. These test values **cannot be changed** until you finish the experiment (A/B test value has the highest priority).&#x20;
{% endhint %}

The priority order for parameter value source is the following:

1. **Top priority** – A/B test parameter value.
2. Remote config value from devtodev server.
3. Least priority – Default value defined in the application code.

For now, if you would like to check which parameter configuration performs better during an A/B test, you need to do the following:&#x20;

1. Before launching an A/B test, create a condition with the same audience that will be used in the test.&#x20;
2. Create a parameter configuration that you would like to test with this condition.&#x20;
3. Publish changes. The configuration will be sent to user devices.&#x20;
4. Launch the A/B test. The SDK will update the configuration on device and all events will be marked with an A/B test group. &#x20;

## Limitations

* For now, you cannot use Custom events to define a [**condition**](#conditions) audience. However, you can create a [user segment](/reports-and-functionality/project-related-reports-and-fuctionality/users#segments) with the necessary event and use this segment instead.&#x20;
* You cannot see the current configuration in the user card.&#x20;
* The [changelog](#changelog) stores around 200 configuration versions.


# Autocapture

Autocapture is a set of features in devtodev that allows you to collect data automatically, without manually sending events from the client side. It is designed to simplify integration, reduce engineering effort, and ensure data consistency across platforms.

Currently, Autocapture supports purchase tracking, refund tracking, and AI-assisted integration. Web auto-tracking is in development.

## How it works

Each Autocapture feature collects data from validated sources such as the App Store, Google Play, or SDK behavior. In most cases, you only need to enable the feature in the devtodev settings and initialize the SDK accordingly. No additional coding is required.

## How to set up

* [Automatic payment tracking](/integration/autocapture/automatic-payment-tracking)
* [Automatic refund tracking](/integration/autocapture/automatic-refund-tracking)
* [AI-assisted integration for Unity](/integration/integration-of-sdk-v2/sdk-integration/unity/ai-assisted-integration-beta)&#x20;

## Compatibility and limitations

* Autocapture is available only for native payment systems (Google Play and App Store).
* Unity Android refund tracking is not supported due to store receipt limitations.
* Do not mix manual and automatic event tracking for the same transaction types.
* Web tracking will be available via the [devtodev Web SDK](https://docs.devtodev.com/integration/integration-of-sdk-v2/sdk-integration/web) (currently in development).


# Automatic payment tracking

Devtodev allows you to collect payment and subscription information automatically for the Google Play and App Store platforms. All you need to do is enable the corresponding module during the SDK initialization and configure store setting in the devtodev interface.&#x20;

The automatic payment tracking system will help you avoid integration errors with data submitted using the [Real Payment](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#real-payment) and [Subscription](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#subscriptions) events. This system will also help you avoid sending fraudulent transaction data, as each transaction is additionally verified on the Google Play and App Store platforms before being recorded in the database.&#x20;

{% hint style="warning" %}
**Important**: Automatic payment tracking is only available for the Google Play and App Store platforms, and only if you are using their native payment systems. \
If you are using a third-party payment system, you will need to integrate the [Real Payment](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#real-payment) Basic event.&#x20;
{% endhint %}

{% hint style="info" %}
Please note that you should not simultaneously use an automatic payment tracking system and send payment data using the integrated [Real Payment](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#real-payment) and [Subscription](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#subscriptions) events, as this may result in duplicate payment data. \
Devtodev will do its best to prevent including such duplicates in the statistics; however, for some integration cases, this is impossible (e.g. when using *Google Play Purchase Token* instead of *Transaction ID* in Real Payment event data). \
Therefore, when you switch to automatic payment tracking, we recommend that you remove the integration of [Real payment](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#real-payment) and [Subscription](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#subscriptions) events, except in the case of tracking transactions with third-party payment systems.
{% endhint %}

{% hint style="warning" %}
While automatic payment tracking helps avoid fraudulent transaction data, this mechanism **does not automatically mark users with such payments as cheaters**. In case you would like to mark users as cheaters, use a [dedicated method](/integration/integration-of-sdk-v2/setting-up-events/user-profile#cheater) or [flag the User card](/reports-and-functionality/project-related-reports-and-fuctionality/users#mark-as-cheater-or-tester).
{% endhint %}

{% content-ref url="/pages/JSTvq6SDrJt7h9ocqtwk" %}
[App Store](/integration/autocapture/automatic-payment-tracking/app-store)
{% endcontent-ref %}

{% content-ref url="/pages/Qp0MFkkMwNNxUoVJ2e9Y" %}
[Google Play](/integration/autocapture/automatic-payment-tracking/google-play)
{% endcontent-ref %}

{% content-ref url="/pages/LHz5EuRgatEcP0d1Ltjf" %}
[Aghanim](/integration/autocapture/automatic-payment-tracking/aghanim)
{% endcontent-ref %}


# App Store

{% stepper %}
{% step %}

#### [SDK Integration](#sdk-integration-1)

{% endstep %}

{% step %}

#### [Settings on the App Store Connect side](#settings-on-app-store-connect-side)

{% endstep %}

{% step %}

#### [Settings on devtodev side](#settings-on-devtodev-side-1)

[For In-App Purchases](#in-app-purchases)&#x20;

[For Subscriptions](#subcriptions)&#x20;
{% endstep %}
{% endstepper %}

## SDK integration

{% hint style="info" %}
**Prerequisites:**

**Native SDK**

* StoreKit SDK v1
* DTDAnalytics SDK **2.5.0** and higher

**Unity**

* DTDAnalytics **3.9.0** and higher
* [DTDPurchases](https://github.com/devtodev-analytics/package_Purchases/releases) **3.9.0** and higher

Check [Initialization](#initialization) when moving to Unity IAP 5.0.4 and higher!&#x20;
{% endhint %}

### Initialization

To initialize the service, call the `startAutoTracking` method of the `DTDPurchases` interface. Call the method after the SDK initialization call.&#x20;

See examples below:

{% tabs fullWidth="false" %}
{% tab title="Swift" %}

```swift
let config = DTDAnalyticsConfiguration()
DTDAnalytics.initialize(applicationKey: "App ID", configuration: config)
DTDPurchases.startAutoTracking()
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
DTDAnalyticsConfiguration *config;
[DTDAnalytics applicationKey:@"App ID" configuration:config];
[DTDPurchases startAutoTracking];
```

{% endtab %}

{% tab title="Unity" %}
**For Unity IAP below 5.0**

```csharp
DTDAnalytics.Initialize("App ID", new DTDAnalyticsConfiguration
{
  UserId = userId,
  ApplicationVersion = Application.version,
  CurrentLevel = lvl,
  LogLevel = DTDLogLevel.No,
  TrackingAvailability = DTDTrackingStatus.Enable
});
DTDPurchases.StartAutoTracking();
```

***

**For Unity IAP 5.0.4 and higher (5.1 and higher recommended)**

Add `StoreKitSelector.forceStoreKit1 = true;` before IAP initialization:

```csharp
    public async Task InitializePurchasing()
    {
        StoreKitSelector.forceStoreKit1 = true;
        storeController = UnityIAPServices.StoreController();
        // UnityIAPServices initialization
        await storeController.Connect();
    }
```

After that, initialize devtodev SDK&#x20;

```csharp
DTDAnalytics.Initialize("App ID", new DTDAnalyticsConfiguration
{
  UserId = userId,
  ApplicationVersion = Application.version,
  CurrentLevel = lvl,
  LogLevel = DTDLogLevel.No,
  TrackingAvailability = DTDTrackingStatus.Enable
});
DTDPurchases.StartAutoTracking();
```

{% endtab %}
{% endtabs %}

### **Subscription restore**

Subscription renewals, cancellations, and other status changes are tracked using server-to-server data from the App Store. See the following sections for setup.&#x20;

In order to be able to track status changes for subscriptions that were issued prior to the devtodev SDK integration, you must send the history of previously purchased user subscriptions to devtodev.&#x20;

The SDK keeps track of the need to send this historical data so that it does not make “unnecessary” requests to the App Store. Use the `isRestoreTransactionHistoryRequired` method to check whether it is necessary to send data about previously purchased subscriptions to devtodev.&#x20;

The `isRestoreTransactionHistoryRequired` method returns a `BOOL` value.&#x20;

Here is an example of a purchase history request with the check:

{% tabs fullWidth="false" %}
{% tab title="Swift" %}

```swift
DTDAnalytics.isRestoreTransactionHistoryRequired { isNeedRestore in
  if isNeedRestore {
    DispatchQueue.main.async {
      SKPaymentQueue.default().restoreCompletedTransactions()
    }
  }
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[DTDAnalytics isRestoreTransactionHistoryRequiredWithCompletionHandler:^(BOOL isNeedRestore){
  if (isNeedRestore == true) {
    dispatch_async(dispatch_get_main_queue(), ^{
      [SKPaymentQueue.defaultQueue restoreCompletedTransactions];
    });
  }
}];
```

{% endtab %}

{% tab title="Unity" %}
{% code fullWidth="true" %}

```csharp
if (Application.platform == RuntimePlatform.IPhonePlayer || Application.platform == RuntimePlatform.OSXPlayer)
{
  var apple = storeExtensionProvider.GetExtension<IAppleExtensions>();
  DTDAnalytics.IsRestoreTransactionHistoryRequired((required) =>
  {
    if(!required) return;
    apple.RestoreTransactions((success, text) =>
    {
      Debug.Log(success ? $"Purchases restored successfully on iOS." : $"{text}");
    });
  });
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% hint style="info" %}
`DTDPurchases` service will get the information about previously purchased subscriptions automatically from `SKPaymentQueue`. Sending the data via the [`subscriptionHistory`](/integration/integration-of-sdk-v2/setting-up-events/basic-methods#subscriptions) method is not required.&#x20;
{% endhint %}

## **Settings on the App Store Connect side** <a href="#settings-on-app-store-connect-side" id="settings-on-app-store-connect-side"></a>

To get detailed information about the transaction, devtodev requires access to the App Store Server API. To grant this access, you will need to generate an **In-App Purchase API key**.

Generating the key:

1. Authorize on [App Store Connect](https://appstoreconnect.apple.com/).
2. Navigate to the Users and Access section.

   <figure><img src="/files/nlCW7KobBAoN8OCGEUjt" alt=""><figcaption></figcaption></figure>
3. In the **Integrations** tab (1), select **In-App Purchase** from the menu on the left (2), and click `(+)` to add the key (3).

   <figure><img src="/files/U5wjgYYJkkc5R7k0rZHj" alt=""><figcaption></figcaption></figure>
4. Specify the name of the key, for example: "devtodev API Key", and click `Generate`.

   <figure><img src="/files/5KfKfAQLlIezTSkpzYwF" alt=""><figcaption></figcaption></figure>
5. To grant the necessary access to the devtodev service, it is necessary to pass  the following information:

   * Issuer ID
   * Key ID
   * The generated **.p8** file of the In-App Purchase key&#x20;
   * Bundle identifier of the application (App Bundle ID)

   <figure><img src="/files/eMi3yGSD3KvTOGjwPU3m" alt=""><figcaption></figcaption></figure>

## **Settings on devtodev side** <a href="#settings-on-devtodev-side" id="settings-on-devtodev-side"></a>

### In-App Purchases

1. Go to **Settings → Payments integration → IAP auto tracking → Integrate**.<br>

   <figure><img src="/files/eeOumjr6wxoCxHnvpNbj" alt=""><figcaption></figcaption></figure>
2. Fill in the integration form with the data [obtained earlier from the App Store](#settings-on-app-store-connect-side):&#x20;

   * App Bundle ID
   * Issuer ID
   * Key ID
   * Upload the **.p8** file of the In-App Purchase key.

   <figure><img src="/files/PM8oCESR9qShxGsyZwG4" alt=""><figcaption></figcaption></figure>
3. When the integration is complete, the status will change to **Active**. <br>

   <figure><img src="/files/HenP9fnq2nbropEvbqjS" alt=""><figcaption></figcaption></figure>

### Subcriptions

1. In order for IAP auto tracking to work correctly with subscriptions, you need to integrate App Store Server Notification. Go to **Settings → Payments integration → Subscriptions → Integrate**. <br>

   <figure><img src="/files/SNCTCUQvdbjrXT0qPtpt" alt=""><figcaption></figcaption></figure>
2. Fill in the iOS Bundle ID and copy the **Endpoint URL**.<br>

   <figure><img src="/files/Jj0lirFa3KqD3S9TL6N0" alt=""><figcaption></figcaption></figure>
3. Navigate to [App Store Connect](https://appstoreconnect.apple.com/). In the left menu column, under **General**, go to **App information**. Paste the copied endpoint URL into the **Production Server UR**L and **Sandbox Server URL** fields. \
   Click `Save` to confirm your changes.

<figure><img src="/files/DW8stn6DttmW1ygSGiBI" alt=""><figcaption></figcaption></figure>


# Google Play

{% stepper %}
{% step %}

#### [SDK Integration](#sdk-integration-1)

{% endstep %}

{% step %}

#### [Settings in Google Cloud Platform Console](#settings-in-google-cloud-platform-console-1)

[Enable Google Play Android Developer API](#google-play-android-developer-api)&#x20;

[Set up Service account](#service-account) &#x20;

[Set up Pub/Sub service](#pub-sub-service)&#x20;
{% endstep %}

{% step %}

#### [Settings on devtodev side](#step-2.-settings-on-devtodev-side)

{% endstep %}

{% step %}

#### [Settings in Google Play Console](#step-3.-google-paly-console)

[Add service account and grant permissions](#service-account-and-user-permissions)&#x20;

[Set up Real-time developer notifications](#monetisation-setup)&#x20;
{% endstep %}
{% endstepper %}

## SDK integration

{% tabs fullWidth="false" %}
{% tab title="Android native (Kotlin)" %}
{% hint style="info" %}
**Prerequisites:**

* DTDAnalytics **2.5.0** and higher
  {% endhint %}

```kotlin
dependencies {
    implementation ("com.devtodev:android-google-purchases:*.*.*")
    // Starting from version 6.0.0
    implementation ("com.android.billingclient:billing:*.*.*")
}
```

{% endtab %}

{% tab title="Android native (Groovy)" %}
{% hint style="info" %}
**Prerequisites:**

* DTDAnalytics **2.5.0** and higher
  {% endhint %}

```groovy
dependencies {
    implementation 'com.devtodev:android-google-purchases:*.*.*'
    // Starting from version 6.0.0
    implementation 'com.android.billingclient:billing:*.*.*'
}
```

{% endtab %}

{% tab title="Unity" %}
{% hint style="info" %}
**Prerequisites:**

* DTDAnalytics **3.9.0** and higher
* [DTDPurchases](https://github.com/devtodev-analytics/package_Purchases/releases) **3.9.0** and higher
  {% endhint %}
  {% endtab %}
  {% endtabs %}

To initialize the service, call the `startAutoTracking` method of the `DTDPurchases` interface. Call the method after the SDK initialization call:

{% tabs fullWidth="false" %}
{% tab title="Kotlin" %}

```kotlin
val config = DTDAnalyticsConfiguration()
DTDAnalytics.initialize("App ID", config, context)
DTDGooglePurchases.initialize(context)
DTDGooglePurchases.startAutoTracking()
```

{% endtab %}

{% tab title="Java" %}

```java
DTDAnalyticsConfiguration config = new DTDAnalyticsConfiguration();
DTDAnalytics.INSTANCE.initialize("App ID", config, context);
DTDGooglePurchases.INSTANCE.initialize(context);
DTDGooglePurchases.INSTANCE.startAutoTracking();
```

{% endtab %}

{% tab title="Unity (C#)" %}

```csharp
DTDAnalytics.Initialize("App ID", new DTDAnalyticsConfiguration
{
  UserId = userId,
  ApplicationVersion = Application.version,
  CurrentLevel = lvl,
  LogLevel = DTDLogLevel.No,
  TrackingAvailability = DTDTrackingStatus.Enable
});
DTDPurchases.StartAutoTracking();
```

{% endtab %}
{% endtabs %}

### **Subscription restore**

The SDK will automatically restore the list of previously purchased subscriptions at the first launch of the autotracking service. No additional integration is required.

## Settings in Google Cloud Platform Console

{% hint style="info" %}
If you have already activated the [Google Play Android Developer API](#google-play-android-developer-api) and set up a [service account](#service-account) to work with devtodev, you can skip this section and proceed to [Pub/Sub service setup](#pub-sub-service)**.**
{% endhint %}

### Google Play Android Developer API <a href="#google-play-android-developer-api" id="google-play-android-developer-api"></a>

1. Go to [Google Cloud Console](<https://console.cloud.google.com/ >) under your Google account.\
   Select the project (1) for which you want to configure **Google Play Developer API**. Then go to the **APIs and services** section (2).

<figure><img src="/files/xiyRovoRMiE0JMQ3LJ2n" alt=""><figcaption></figcaption></figure>

2. Go to the **Library** section.

<figure><img src="/files/t7DhYEOXoLRxTvj7zzh2" alt=""><figcaption></figcaption></figure>

3. Find the **Google Play Developer API** section.

<figure><img src="/files/2NcZxJsjz4MrzD9S2b4P" alt=""><figcaption></figcaption></figure>

4. Press `ENABLE` to enable the **Google Play Androd Developer API**.

<figure><img src="/files/a1wy97hjEQ699IKVXkOB" alt=""><figcaption></figcaption></figure>

### **Service account** <a href="#service-account" id="service-account"></a>

Go to the [**Service accounts**](https://console.cloud.google.com/apis/credentials/serviceaccountkey) section. &#x20;

1. Select the **project** to link the service account and where you will collect subscription data later. Create a new project if it is not in the list.&#x20;

   &#x20;

   <figure><img src="/files/uFq3w7adMIa1z5OALtNV" alt=""><figcaption></figcaption></figure>
2. In the list of available service keys, click `CREATE SERVICE ACCOUNT`.&#x20;

   &#x20;

   <figure><img src="/files/V2qyWwXCp7AeRXzZFJEc" alt=""><figcaption></figcaption></figure>
3. Fill in the **Service account name** field. The **Service Account ID** field will be filled in automatically (use the screenshot below as an example) and click `CREATE AND CONTINUE`.&#x20;

   &#x20;

   <figure><img src="/files/z8Gy0m68VIu0zq8iXQNK" alt=""><figcaption><p>s</p></figcaption></figure>
4. Skip the optional steps 2-3 and click `DONE` at the end of the form.&#x20;

   &#x20;

   <figure><img src="/files/ftFBvffIooTkWkooAOK4" alt=""><figcaption></figcaption></figure>
5. After creating a service account, you will be returned to the available Service accounts list. \
   \
   Select the created account and open the **KEYS** tab (1), click `ADD KEY` (2), and then select the **JSON** key type option (3) in the pop-up window. \
   Click `CREATE` to confirm your choice. \
   \
   The generated **private key file** will be downloaded automatically. You will need to upload this key to devtodev later in the following steps (see [Settings on devtodev side](#step-2.-settings-on-devtodev-side)).&#x20;

   &#x20;

   <figure><img src="/files/Brs2xwOeOeigB04cnkPd" alt=""><figcaption></figcaption></figure>
6. Go to the **IAM** section (1) in the **Permissions** tab and click `GRANT ACCESS` (2) to add roles to the service account. \
   In the **New principals** section (3), enter the service account address and grant it the **Pub/Sub Subscriber** role (4). \
   If you do not have this role, you need to activate the **Pub/Sub service** in the Cloud Console ([activate Pub/Sub API](https://console.cloud.google.com/apis/api/pubsub.googleapis.com)). \
   Save the changes (5).

   <figure><img src="/files/1goFKAfnoWhwZjAhw1p6" alt=""><figcaption></figcaption></figure>

### **Pub/Sub service** <a href="#pub-sub-service" id="pub-sub-service"></a>

Go to the [list of topics](< https://console.cloud.google.com/cloudpubsub/topic/list>) in the **Pub/Sub** service.

1. Select the project where you would like to collect subscription data. Click `CREATE TOPIC`.&#x20;

   &#x20;

   <figure><img src="/files/FODj0It3t1GjvarkvPPQ" alt=""><figcaption></figcaption></figure>
2. To create a topic: fill in the **Topic ID** (use the screenshot below as an example), disable **Add a default subscription** option (1) and click `CREATE` (2).&#x20;

   &#x20;

   <figure><img src="/files/OBXkQ0agqBz5bnIBwcjx" alt=""><figcaption></figcaption></figure>
3. Select a topic, click on three dots and select **View permissions**.

   &#x20;

   <figure><img src="/files/pHLPRSITaMYt0GRfzpb8" alt=""><figcaption></figcaption></figure>
4. Click `ADD PRINCIPAL` to add the service account. &#x20;

   &#x20;

   <figure><img src="/files/caUJRvMOjW0RvWua0XnM" alt=""><figcaption></figcaption></figure>
5. Add the `google-play-developer-notifications@system.gserviceaccount.com` service account and grant it the role of **Pub/Sub Publisher**. Save the changes.

   &#x20;

   <figure><img src="/files/71F92bF58scV5UCUVZkk" alt=""><figcaption></figcaption></figure>
6. Open the list of subscriptions and click `CREATE SUBSCRIPTION`.

   &#x20;

   <figure><img src="/files/D0IzviX6WueS5tGijy3S" alt=""><figcaption></figcaption></figure>
7. In the appeared form:&#x20;
   * Specify the **Subscription ID** (1).&#x20;
   * Fill in the Topic name in **Select a Cloud Pub/Sub topic** field (2).&#x20;
   * In the **Delivery type** select **Push** (3)**.**&#x20;
   * Copy **Endpoint URL** from devtodev (see steps 8-9) and insert it in the **Endpoint URL** field (4).  \
     \
     All other parameters remain unchanged. Save the changes.

     &#x20;

     <div data-full-width="false"><figure><img src="/files/7HHhXoHka6yadXVJl5v5" alt=""><figcaption></figcaption></figure></div>
8. To get **Endpoint URL,** go to [devtodev](https://analytics.devtodev.com/), select the same app and open app settings (**Settings** → **Payments integration** → **Subscriptions**). Click `Integrate`.

   <figure><img src="/files/VAHbHcOCXpaAoO4PPpnZ" alt=""><figcaption></figcaption></figure>
9. Copy the **Endpoint URL** and paste it in the corresponding field in Google Cloud (step 7).&#x20;

   <figure><img src="/files/KVgnzYPaM3Jooc5cZJat" alt=""><figcaption></figcaption></figure>

## **Settings on devtodev side** <a href="#step-2.-settings-on-devtodev-side" id="step-2.-settings-on-devtodev-side"></a>

1. Go to **Settings → Payments integration → IAP auto tracking** and click `edit` (3).<br>

   <figure><img src="/files/2oMr1kHme7l0tWsz4Qmt" alt=""><figcaption></figcaption></figure>
2. Fill in the integration form with the data obtained earlier:

   * Android App ID
   * Upload the **Private key file** obtained in [**Service Account**](#service-account) step.

   Click `Save`.

   <figure><img src="/files/4syrOVzH5HYqNns1G2H6" alt=""><figcaption></figcaption></figure>
3. When the integration is complete, the status will change to **Active.**

   <figure><img src="/files/HV3zE1cYSJm2vzxrpfHh" alt=""><figcaption></figcaption></figure>
4. If you use subscriptions, go to **Payments integration** → **Subscriptions** and click `Integrate`.&#x20;
5. Upload the **Private key file** obtained in [Service Account](#service-account) step (1). \
   Specify Android App ID (2). \
   Add all existing subscriptions, specifying the bundle, subscription period, and name for each subscription. \
   Click `Save` (3) to finish integration.&#x20;

   <figure><img src="/files/OgiUloHWOHGVda8VFlmP" alt=""><figcaption></figcaption></figure>

## Settings in **Google Play Console** <a href="#step-3.-google-paly-console" id="step-3.-google-paly-console"></a>

### Service account and User permissions <a href="#service-account-and-user-permissions" id="service-account-and-user-permissions"></a>

To set up user permission, you need to log into your Google Play Console account and invite your [previously created service account](#service-account).

1. Sign into the [Google Play Developer Console](https://play.google.com/apps/publish/signup/).

   <figure><img src="/files/JVNYzpkOyAdX6QvDIrPL" alt="" width="375"><figcaption></figcaption></figure>
2. In the **Users and permissions** section, you can add users and manage their permissions.
   * Go to **Users and permissions.**
   * Click `Invite new users`.<br>

     &#x20;

     <figure><img src="/files/B8Phs1kDCYlA5hQFyiXh" alt=""><figcaption></figcaption></figure>
3. Add [Service Account](#service-account) as a new user:&#x20;

   * Add your service account email in the **Email address** field (1).&#x20;
   * In the **Permissions section** (2) select **the apps** (3) accessible to the service account. Click `Apply` (5). \
     Grant permissions to individual apps, or use account permissions to grant access to all apps in your developer account.&#x20;
   * Grant the necessary rights to perform actions (see next step **Account permissions**).&#x20;

   <figure><img src="/files/HG0bnNXUu4tJG8n702oI" alt=""><figcaption></figcaption></figure>
4. Open **Account permissions** tab. Grant the following permissions:

   * View app information (1)
   * View app quality information (2)
   * View financial data (3)

   Click `Apply` to confirm.&#x20;

{% hint style="info" %}
**Please note that the specified permissions may not apply immediately!**
{% endhint %}

<figure><img src="/files/6M1uGuLP5FS3uMByETw5" alt=""><figcaption></figcaption></figure>

5. Click `Invite user` to finish the user setup.&#x20;

### Monetisation setup

After [creating a subscription](#pub-sub-service) in the **Pub/Sub** **service**, you'll need to register the created topic as a target for **Real-time developer notifications** (RTDN).&#x20;

1. Go to the app’s **Monetise with Play** → **Monetisation setup** section.&#x20;
2. Enter the full name of the topic in the **Topic name** field.&#x20;
3. Tick the **Enable real-time notifications** checkbox.
4. Send a **test notification** using the link below (4). If everything is configured correctly, the test notification will change the integration status of the **Subscriptions** in devtodev to Active (Settings → Payments integration → Subscriptions → Market connection).&#x20;
5. Click `Save changes` to finish the setup.<br>

<figure><img src="/files/i3mbSMORMqttExZmjrlG" alt=""><figcaption></figcaption></figure>


# Aghanim

{% hint style="warning" %}
This integration is configured only in [Aghanim](https://docs.aghanim.com/aghanim-connect/devtodev/).&#x20;
{% endhint %}

By connecting Aghanim with devtodev, you can track player-generated events from the Game Hub, ensuring precise tracking of user actions across your entire game environment.&#x20;

## Step 1: Configure the Integration in Aghanim <a href="#step-1-configure-the-integration-in-aghanim" id="step-1-configure-the-integration-in-aghanim"></a>

1. Go to the Aghanim Dashboard → **Aghanim Connect** → [**Devtodev**](https://dashboard.aghanim.com/go/app-connect/devtodev).
2. Click the **Install** button to enable the integration.

## Step 2: Add devtodev attributes to `player.verify` webhook response <a href="#step-2-add-devtodev-attributes-to-playerverify-webhook-response" id="step-2-add-devtodev-attributes-to-playerverify-webhook-response"></a>

To ensure devtodev correctly identifies users and attributes their actions on the Game Hub, include the devtodev-specific attributes in the [`player.verify`](https://docs.aghanim.com/webhooks/verify-player) webhook response:

<table><thead><tr><th width="263">Key</th><th width="156">Type</th><th width="224">Description</th><th>Required?</th></tr></thead><tbody><tr><td><code>attributes.devtodev_apikey</code></td><td><code>string</code></td><td>The API key of the project in devtodev. Obtain the API key by going to Settings → SDK → <a href="/pages/-M1exVsY_QpCEDgJnHzW#integration">Integration</a> in the application menu.</td><td><strong>Yes</strong></td></tr><tr><td><code>attributes.devtodev_device_id</code></td><td><code>DevtodevDeviceId</code></td><td>The device ID object.</td><td><strong>Yes</strong></td></tr></tbody></table>

## **The `DevtodevDeviceId` schema**

The device ID object must contain at least one of the following identifiers:

<table><thead><tr><th width="189">Key</th><th width="161">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>devtodevId</code></td><td><code>number</code></td><td>The devtodev ID is the primary numeric identifier for the device/user account in the devtodev database. The <strong>devtodevId</strong> can be obtained from the <a href="/pages/-MhDprH27Nb11KM0omop#getting-devtodev-id">devtodev SDK</a>.</td></tr><tr><td><code>userId</code></td><td><code>string</code></td><td>A <strong>custom user ID</strong> assigned by the developer. Usually, a user ID on the developer's server. The ID must be specified during <a href="/pages/-MhDojIrD6ZBtbz-Umdg#sdk-initialization">devtodev SDK initialization</a>, or specified using the <a href="/pages/-MhDpwXXuGNEV5GixSHe#user-id">SetUserID method</a>.</td></tr><tr><td><code>advertisingId</code></td><td><code>string</code></td><td>The Advertising ID or IDFA of the user device.</td></tr></tbody></table>

## **Example webhook response**

```json
{
  "player_id": "2D2R-OP3C",
  "name": "Beebee-Ate",
  "level": 42,
  "attributes": {
    "devtodev_apikey": "ak-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "devtodev_device_id": {
      "devtodevId": 123456,
      "userId": "2D2R-OP3C",
      "advertisingId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    },
  }
}
```

Once the integration is set up, Aghanim will automatically send [Real Payments](/integration/server-api/data-api-2.0#real-currency-payment) events to devtodev, allowing you to track user purchases from the game hub.&#x20;


# Automatic refund tracking

Devtodev allows you to collect refund data automatically for purchases made through the Google Play and App Store platforms. Refunds are recorded as transactions with negative amounts and matched to original purchases whenever possible.

The automatic refund tracking system helps you monitor actual revenue more accurately and analyze refund patterns without manual event configuration.

{% hint style="warning" %}
Automatic refund tracking is available only for the **Google Play** and **App Store** platforms, and only for purchases made using their native payment systems.\
If you are using a third-party payment system, you need to send refunds manually using the **Real Payment** event with a negative amount.
{% endhint %}

{% hint style="info" %}
If you're using **Unity** and sending `Real Payment` events manually, refunds may not be processed correctly. This is because the purchase token is partially truncated in the SDK, which prevents it from being matched with store refunds.

We strongly recommend enabling [**automatic payment tracking**](/integration/autocapture/automatic-payment-tracking) to ensure correct refund matching on Unity for Android.
{% endhint %}

## How it works

Refunds are collected via store APIs or postbacks and processed on the server side. For each refund:

* devtodev attempts to match it to an original transaction by transaction ID.
* The refund is saved as a transaction with a negative amount.
* The event is dated using the refund processing date (not the original payment date).

{% hint style="info" %}
**Note**: Refund tracking works automatically — **no changes in the SDK** are required.
{% endhint %}

## Setup

{% hint style="success" %}
We recommend using automatic refund tracking **together with** [**automatic payment tracking**](/integration/autocapture/automatic-payment-tracking) to ensure accurate matching and avoid inconsistencies in reports.
{% endhint %}

To enable refund tracking:

* Set up integration with the App Store or Google Play.
* Go to: `Settings → Payments integration → IA refunds tracking`.
* Click the ✎ icon next to IA refunds tracking and fill in the fields related to the platform.
* Refund tracking starts working automatically on supported platforms after you enable it in the devtodev interface.
* No SDK-side changes are required.

For details on how to set up integration with app stores, see:

{% columns %}
{% column %}
{% content-ref url="/pages/N40RRGpRNA41MFTUmSpI" %}
[App Store](/integration/autocapture/automatic-refund-tracking/app-store)
{% endcontent-ref %}
{% endcolumn %}

{% column %}
{% content-ref url="/pages/jusqTChn7AUnnK12KSeQ" %}
[Google Play](/integration/autocapture/automatic-refund-tracking/google-play)
{% endcontent-ref %}
{% endcolumn %}
{% endcolumns %}


# App Store

{% stepper %}
{% step %}

#### [Settings on the App Store Connect side](#settings-on-app-store-connect-side)

{% endstep %}

{% step %}

#### [Settings on devtodev side](#settings-on-devtodev-side-1)

{% endstep %}
{% endstepper %}

## **Settings on the App Store Connect side** <a href="#settings-on-app-store-connect-side" id="settings-on-app-store-connect-side"></a>

To get detailed information about the transaction, devtodev requires access to the App Store Server API. To grant this access, you will need to generate an **In-App Purchase API key**.

Generating the key:

1. Authorize on [App Store Connect](https://appstoreconnect.apple.com/).
2. Navigate to the Users and Access section.

   <figure><img src="/files/nlCW7KobBAoN8OCGEUjt" alt=""><figcaption></figcaption></figure>
3. In the **Integrations** tab (1), select **In-App Purchase** from the menu on the left (2), and click `(+)` to add the key (3).

   <figure><img src="/files/U5wjgYYJkkc5R7k0rZHj" alt=""><figcaption></figcaption></figure>
4. Specify the name of the key, for example: "devtodev API Key", and click `Generate`.

   <figure><img src="/files/5KfKfAQLlIezTSkpzYwF" alt=""><figcaption></figcaption></figure>
5. To grant the necessary access to the devtodev service, it is necessary to pass  the following information:

   * Issuer ID
   * Key ID
   * The generated **.p8** file of the In-App Purchase key&#x20;
   * Bundle identifier of the application (App Bundle ID)

   <figure><img src="/files/eMi3yGSD3KvTOGjwPU3m" alt=""><figcaption></figcaption></figure>

## **Settings on devtodev side** <a href="#settings-on-devtodev-side" id="settings-on-devtodev-side"></a>

1. Go to `Settings → Payments integration → IA refunds tracking`.<br>

   <figure><img src="/files/YG8SvD5UWexGxjuJZP2a" alt=""><figcaption></figcaption></figure>
2. Fill in the integration form with the data [obtained earlier from the App Store](#settings-on-app-store-connect-side):&#x20;

   * App Bundle ID
   * Issuer ID
   * Key ID
   * Upload the **.p8** file of the In-App Purchase key.

   <figure><img src="/files/UskYX2XHt2HpxEbWeFP5" alt="refunds step 2 ios"><figcaption></figcaption></figure>
3. When the integration is complete, the status will change to **Active**.&#x20;

   <figure><img src="/files/Knth3ij0eyH37wBRs1MC" alt="refunds step 3 ios"><figcaption></figcaption></figure>


# Google Play

{% stepper %}
{% step %}

#### [Settings in Google Cloud Platform Console](#settings-in-google-cloud-platform-console-1)

[Enable Google Play Android Developer API](#google-play-android-developer-api)&#x20;

[Set up Service account](#service-account) &#x20;

[Set up Pub/Sub service](#pub-sub-service)&#x20;
{% endstep %}

{% step %}

#### [Settings on devtodev side](#step-2.-settings-on-devtodev-side)

{% endstep %}

{% step %}

#### [Settings in Google Play Console](#step-3.-google-paly-console)

[Add service account and grant permissions](#service-account-and-user-permissions)&#x20;

[Set up Real-time developer notifications](#monetisation-setup)&#x20;
{% endstep %}
{% endstepper %}

## Settings in Google Cloud Platform Console

{% hint style="info" %}
If you have already activated the [Google Play Android Developer API](#google-play-android-developer-api) and set up a [service account](#service-account) to work with devtodev, you can skip this section and proceed to [Pub/Sub service setup](#pub-sub-service)**.**
{% endhint %}

### Google Play Android Developer API <a href="#google-play-android-developer-api" id="google-play-android-developer-api"></a>

1. Go to [Google Cloud Console](<https://console.cloud.google.com/ >) under your Google account.\
   Select the project (1) for which you want to configure **Google Play Developer API**. Then go to the **APIs and services** section (2).

<figure><img src="/files/xiyRovoRMiE0JMQ3LJ2n" alt=""><figcaption></figcaption></figure>

2. Go to the **Library** section.

<figure><img src="/files/t7DhYEOXoLRxTvj7zzh2" alt=""><figcaption></figcaption></figure>

3. Find the **Google Play Developer API** section.

<figure><img src="/files/2NcZxJsjz4MrzD9S2b4P" alt=""><figcaption></figcaption></figure>

4. Press `ENABLE` to enable the **Google Play Androd Developer API**.

<figure><img src="/files/a1wy97hjEQ699IKVXkOB" alt=""><figcaption></figcaption></figure>

### **Service account** <a href="#service-account" id="service-account"></a>

Go to the [**Service accounts**](https://console.cloud.google.com/apis/credentials/serviceaccountkey) section. &#x20;

1. Select the **project** to link the service account and where you will collect subscription data later. Create a new project if it is not in the list.&#x20;

   &#x20;

   <figure><img src="/files/uFq3w7adMIa1z5OALtNV" alt=""><figcaption></figcaption></figure>
2. In the list of available service keys, click `CREATE SERVICE ACCOUNT`.&#x20;

   &#x20;

   <figure><img src="/files/V2qyWwXCp7AeRXzZFJEc" alt=""><figcaption></figcaption></figure>
3. Fill in the **Service account name** field. The **Service Account ID** field will be filled in automatically (use the screenshot below as an example) and click `CREATE AND CONTINUE`.&#x20;

   &#x20;

   <figure><img src="/files/z8Gy0m68VIu0zq8iXQNK" alt=""><figcaption><p>s</p></figcaption></figure>
4. Skip the optional steps 2-3 and click `DONE` at the end of the form.&#x20;

   &#x20;

   <figure><img src="/files/ftFBvffIooTkWkooAOK4" alt=""><figcaption></figcaption></figure>
5. After creating a service account, you will be returned to the available Service accounts list. \
   \
   Select the created account and open the **KEYS** tab (1), click `ADD KEY` (2), and then select the **JSON** key type option (3) in the pop-up window. \
   Click `CREATE` to confirm your choice. \
   \
   The generated **private key file** will be downloaded automatically. You will need to upload this key to devtodev later in the following steps (see [Settings on devtodev side](#step-2.-settings-on-devtodev-side)).&#x20;

   &#x20;

   <figure><img src="/files/Brs2xwOeOeigB04cnkPd" alt=""><figcaption></figcaption></figure>
6. Go to the **IAM** section (1) in the **Permissions** tab and click `GRANT ACCESS` (2) to add roles to the service account. \
   In the **New principals** section (3), enter the service account address and grant it the **Pub/Sub Subscriber** role (4). \
   If you do not have this role, you need to activate the **Pub/Sub service** in the Cloud Console ([activate Pub/Sub API](https://console.cloud.google.com/apis/api/pubsub.googleapis.com)). \
   Save the changes (5).

   <figure><img src="/files/1goFKAfnoWhwZjAhw1p6" alt=""><figcaption></figcaption></figure>

### **Pub/Sub service** <a href="#pub-sub-service" id="pub-sub-service"></a>

Go to the [list of topics](< https://console.cloud.google.com/cloudpubsub/topic/list>) in the **Pub/Sub** service.

1. Select the project where you would like to collect subscription data. Click `CREATE TOPIC`.&#x20;

   &#x20;

   <figure><img src="/files/FODj0It3t1GjvarkvPPQ" alt=""><figcaption></figcaption></figure>
2. To create a topic: fill in the **Topic ID** (use the screenshot below as an example), disable **Add a default subscription** option (1) and click `CREATE` (2).&#x20;

   &#x20;

   <figure><img src="/files/OBXkQ0agqBz5bnIBwcjx" alt=""><figcaption></figcaption></figure>
3. Select a topic, click on three dots and select **View permissions**.

   &#x20;

   <figure><img src="/files/pHLPRSITaMYt0GRfzpb8" alt=""><figcaption></figcaption></figure>
4. Click `ADD PRINCIPAL` to add the service account. &#x20;

   &#x20;

   <figure><img src="/files/caUJRvMOjW0RvWua0XnM" alt=""><figcaption></figcaption></figure>
5. Add the `google-play-developer-notifications@system.gserviceaccount.com` service account and grant it the role of **Pub/Sub Publisher**. Save the changes.

   &#x20;

   <figure><img src="/files/71F92bF58scV5UCUVZkk" alt=""><figcaption></figcaption></figure>
6. Open the list of subscriptions and click `CREATE SUBSCRIPTION`.

   &#x20;

   <figure><img src="/files/D0IzviX6WueS5tGijy3S" alt=""><figcaption></figcaption></figure>
7. In the appeared form:&#x20;
   * Specify the **Subscription ID** (1).&#x20;
   * Fill in the Topic name in **Select a Cloud Pub/Sub topic** field (2).&#x20;
   * In the **Delivery type** select **Push** (3)**.**&#x20;
   * Copy **Endpoint URL** from devtodev (see steps 8-9) and insert it in the **Endpoint URL** field (4).  \
     \
     All other parameters remain unchanged. Save the changes.

     &#x20;

     <div data-full-width="false"><figure><img src="/files/7HHhXoHka6yadXVJl5v5" alt=""><figcaption></figcaption></figure></div>
8. To get **Endpoint URL,** go to [devtodev](https://analytics.devtodev.com/), select the same app and open app settings (`Settings → Payments integration → IA refunds tracking`).<br>

   <figure><img src="/files/YvnItaUrlUsyIfV9LqT0" alt=""><figcaption></figcaption></figure>
9. Copy the **Endpoint URL** and paste it in the corresponding field in Google Cloud (step 7).&#x20;

## **Settings on devtodev side** <a href="#step-2.-settings-on-devtodev-side" id="step-2.-settings-on-devtodev-side"></a>

1. Go to `Settings → Payments integration → IA refunds tracking` and click `edit` (3).<br>

   <figure><img src="/files/ksr4nA1ec4jiC1eTmaeI" alt=""><figcaption></figcaption></figure>
2. Fill in the integration form with the data obtained earlier:

   * Android App ID
   * Upload the **Private key file** obtained in [**Service Account**](#service-account) step.

   Click `Save`.<br>

   <figure><img src="/files/UGjMHiyGWtO0JtjNGfke" alt=""><figcaption></figcaption></figure>
3. When the integration is complete, the status will change to **Active.**

   <figure><img src="/files/fEEGZqO5DBVjddm74E3w" alt=""><figcaption></figcaption></figure>

## Settings in **Google Play Console** <a href="#step-3.-google-paly-console" id="step-3.-google-paly-console"></a>

### Service account and User permissions <a href="#service-account-and-user-permissions" id="service-account-and-user-permissions"></a>

To set up user permission, you need to log into your Google Play Console account and invite your [previously created service account](#service-account).

1. Sign into the [Google Play Developer Console](https://play.google.com/apps/publish/signup/).

   <figure><img src="/files/JVNYzpkOyAdX6QvDIrPL" alt="" width="375"><figcaption></figcaption></figure>
2. In the **Users and permissions** section, you can add users and manage their permissions.
   * Go to **Users and permissions.**
   * Click `Invite new users`.<br>

     &#x20;

     <figure><img src="/files/B8Phs1kDCYlA5hQFyiXh" alt=""><figcaption></figcaption></figure>
3. Add [Service Account](#service-account) as a new user:&#x20;

   * Add your service account email in the **Email address** field (1).&#x20;
   * In the **Permissions section** (2) select **the apps** (3) accessible to the service account. Click `Apply` (5). \
     Grant permissions to individual apps, or use account permissions to grant access to all apps in your developer account.&#x20;
   * Grant the necessary rights to perform actions (see next step **Account permissions**).&#x20;

   <figure><img src="/files/HG0bnNXUu4tJG8n702oI" alt=""><figcaption></figcaption></figure>
4. Open **Account permissions** tab. Grant the following permissions:

   * View app information (1)
   * View app quality information (2)
   * View financial data (3)

   Click `Apply` to confirm.&#x20;

{% hint style="info" %}
**Please note that the specified permissions may not apply immediately!**
{% endhint %}

<figure><img src="/files/6M1uGuLP5FS3uMByETw5" alt=""><figcaption></figcaption></figure>

5. Click `Invite user` to finish the user setup.&#x20;

### Monetisation setup

After [creating a subscription](#pub-sub-service) in the **Pub/Sub** **service**, you'll need to register the created topic as a target for **Real-time developer notifications** (RTDN).&#x20;

1. Go to the app’s **Monetise with Play** → **Monetisation setup** section.&#x20;
2. Enter the full name of the topic in the **Topic name** field.&#x20;
3. Tick the **Enable real-time notifications** checkbox.
4. Send a **test notification** using the link below (4). If everything is configured correctly, the test notification will change the integration status of the **Subscriptions** in devtodev to Active (Settings → Payments integration → Subscriptions → Market connection).&#x20;
5. Click `Save changes` to finish the setup.<br>

<figure><img src="/files/i3mbSMORMqttExZmjrlG" alt=""><figcaption></figcaption></figure>


# Test Devices

In devtodev you can mark a device as a test device. You can do so in the [Users & Segments](/reports-and-functionality/project-related-reports-and-fuctionality/users#mark-as-cheater-or-tester) section or in Settings -> SDK -> [Test Devices](/reports-and-functionality/project-related-reports-and-fuctionality/settings#test-devices).&#x20;

The system will exclude from the reports all incoming events from the test devices (except Real-time dashboard). You can check the evets in Settings -> SDK -> Integration -> [Event Log](/reports-and-functionality/project-related-reports-and-fuctionality/settings#event-log) or in the Users & Segments section.&#x20;

When you mark the User card as a Tester, the log in the User card will not be cached and the events will appear much quicker.&#x20;

If the app is in test mode, all devices with this app are added automatically to the list of test devices (up to 100 users). You can switch to Production mode manually in Settings -> [General Settings](/reports-and-functionality/project-related-reports-and-fuctionality/settings#switch-to-production-mode).

## Users & Segments

In the [Users & Segment](/reports-and-functionality/project-related-reports-and-fuctionality/users) section you can find a specific user using filters. For example, search a specific Advertisig ID or IDFA.&#x20;

<figure><img src="/files/CPEMVIHpI3ZoL2RPvBMB" alt=""><figcaption></figcaption></figure>

Open the User Card and mark the User as a Tester.&#x20;

<figure><img src="/files/m3iNDcX79CzuHJPoEqTz" alt=""><figcaption></figcaption></figure>

## Test Devices&#x20;

You can check and configure your list of test devices in Settings -> SDK -> [Test Devices](/reports-and-functionality/project-related-reports-and-fuctionality/settings#test-devices).&#x20;

<figure><img src="/files/qA39dcSLFTYekmMNqlWn" alt=""><figcaption></figcaption></figure>


# Server API

List of all devtodev APIs

{% content-ref url="/pages/9mREio94PxrhSfonE3Wn" %}
[Data API 2.0](/integration/server-api/data-api-2.0)
{% endcontent-ref %}

{% content-ref url="/pages/QtZ5jx1n4Bbz7kBklXau" %}
[Subscription API](/integration/server-api/subscription-api)
{% endcontent-ref %}

{% content-ref url="/pages/-LyhAF2-5mcNhrLm\_hUx" %}
[Push API](/integration/server-api/push-api)
{% endcontent-ref %}

{% content-ref url="/pages/-LyhAC0Ly5EcTxM-jiNZ" %}
[Raw Export](/integration/server-api/raw-export)
{% endcontent-ref %}

{% content-ref url="/pages/-LyhAIijPNykXmpH2kJv" %}
[Labels API](/integration/server-api/labels-api)
{% endcontent-ref %}

Below you can find APIs for 3rd party services. You can use them in case devtodev does not provide an integration to a specific 3rd party service.&#x20;

Check the list of [available Attribution trackers](/3rd-party-sources/3-rd-party-attribution).

{% content-ref url="/pages/-M3zDt\_eaclnLqZ5dB9H" %}
[Custom postback API](/3rd-party-sources/3-rd-party-attribution/custom-postback-api)
{% endcontent-ref %}


# Data API 2.0

Server API integration manual

If, for some reason, you cannot use the devtodev SDK, or some events cannot be tracked on the client side, you can use the devtodev data API. Using the API, you can send a set of events that almost completely replicates the capabilities of the SDK. However, you will need to independently prepare data about the device/user and also independently track the start and duration of sessions.

## Request format

### URL <a href="#example-url" id="example-url"></a>

`https://api.devtodev.com/v2/analytics/report?appId=sampleApplicationId`

<table><thead><tr><th width="136.33333333333331">Field</th><th width="113">Required</th><th>Description</th></tr></thead><tbody><tr><td>appId</td><td>Yes</td><td>devtodev application ID</td></tr></tbody></table>

### Limitations

All the transferred data must be in UTF8 encoding.

The body must contain JSON with one or more events for one or several users. The message body can be sent either in uncompressed or compressed form, which must be specified using the appropriate value for the "Content-Type" header.

The packet size cannot exceed 2 MB in its uncompressed state. Packages that exceed this size can't be processed.&#x20;

{% hint style="warning" %}
We do not accept data with a `timestamp` more than 7 days in the past or more than 3 hours in the future from the current time. The exception is data regarding actual payments and subscriptions; we will record this data, but it will be dated as of the time it was received by the server. These date restrictions do not apply when importing historical data.
{% endhint %}

### Headers <a href="#example-headers" id="example-headers"></a>

<table><thead><tr><th width="155">Header</th><th width="83">Type</th><th width="104">Required</th><th>Description</th></tr></thead><tbody><tr><td>Content-type</td><td>String</td><td>Yes</td><td><p></p><p>The possible values for the "Content-type" field are:</p><ul><li><strong>application/json -</strong> for sending uncompressed JSON data</li><li><strong>application/zstd -</strong> for sending data compressed using <a href="http://facebook.github.io/zstd/">Facebook's standard algorithm</a></li><li><strong>application/gzip</strong> - for sending data compressed using gzip algorithms</li></ul></td></tr></tbody></table>

### Example POST Data: <a href="#example-post-data" id="example-post-data"></a>

```json
{  
    "reports" : [
        {
            "deviceId" : "deviceId",
            "previousDeviceId" : "samplePreviousMainId",
            "userId" : "sampleUserId",
            "previousUserId" : "samplePreviousUserId",
            "devtodevId": 6123517,
            "packages" : [
                {
                    "language" : "en",
                    "country": "US",
                    "ip": "154.12.121.11",
                    "appVersion" : "1.0",
                    "appBuildVersion": "30",
                    "sdkVersion" : "2.0",
                    "bundle" : "com.example.application",
                    "installationSource" : "",
                    "engine": "Unity",
                    "events" : [
                        ... //see Events section
                    ]
                },
                {
                    ...
                }
            ]  
        },
        ...
    ]
}
```

The **`reports`** object contains:

<table><thead><tr><th width="179">Parameter</th><th width="120">Type</th><th width="194">Required</th><th>Description</th></tr></thead><tbody><tr><td>deviceId</td><td>String (64)</td><td>Yes</td><td>Current (actual) Device ID</td></tr><tr><td>previousDeviceId</td><td>String (64)</td><td>Should be sent if the Device ID is changed</td><td>Previous Device ID</td></tr><tr><td>userId</td><td>String (64)</td><td>When accounting by User ID</td><td>Current (actual) User ID</td></tr><tr><td>previousUserId</td><td>String (64)</td><td>Should be sent if the User ID is changed (renamed)</td><td>Previous User ID</td></tr><tr><td>devtodevId</td><td>Long</td><td>No (If this field is specified, all params above are optional)</td><td>Numeric user/device account ID in the devtodev database. We recommend using this identifier if you are using SDK and API at the same time. <a href="/pages/-MhDprH27Nb11KM0omop#getting-devtodev-id">Get this identifier on the SDK side</a>, save it on your server and use it to send data via API. Sending other identifiers is not necessary in this case.</td></tr><tr><td>packages</td><td>Array</td><td>Yes</td><td>Packets with events that happened to the user (see below).  An array is used because events can be grouped by the platform of a cross-platform project or any other changed parameter that is at the same level as the "events" array.</td></tr></tbody></table>

Each packet consists of the following fields:

<table><thead><tr><th width="185">Parameter</th><th width="84">Type</th><th width="104">Required</th><th>Description</th></tr></thead><tbody><tr><td>platform</td><td>String</td><td>No</td><td><strong>Only required for</strong> <a href="/pages/EBjsmNHf2V14SSZuYNZr"><strong>cross-platform type projects</strong></a><strong>.</strong> <br>Platfrom identifier (Settings -> SDK -> Integration -> Platform ID)</td></tr><tr><td>language</td><td>String (3)</td><td>Yes</td><td>User/device language (ISO 639-1, ISO 639-2, ISO 639-3)</td></tr><tr><td>country</td><td>String (2)</td><td>Only for server-server protocol</td><td>User/device country (ISO_3166-1_alpha-2). You don't need to use this if you're sending the “ip” field.</td></tr><tr><td>ip</td><td>String (15)</td><td>Only for server-server protocol</td><td>The IPv4 address of the device. Used to determine the user's country. You can omit this if you are sending the “country” field, and vice versa.</td></tr><tr><td>appVersion</td><td>String (64)</td><td>No</td><td>App version</td></tr><tr><td>appBuildVersion</td><td>String</td><td>No</td><td>Application build version. A string value is used for compatibility across all platforms</td></tr><tr><td>sdkVersion</td><td>String</td><td>No</td><td>Version of the integrated SDK</td></tr><tr><td>bundle</td><td>String</td><td>No</td><td>Application bundle</td></tr><tr><td>engine</td><td>String</td><td>No</td><td>Application platform/engine. Preset values are: Native, Unity, Unreal, Air.</td></tr><tr><td>installationSource</td><td>String</td><td>No</td><td>Only for Android. The installer bundle is passed as the value. It determines where the application was installed from (apk, Google Play, Amazon, etc). Used to detect and prevent illegal apk installs</td></tr><tr><td>events</td><td>Array</td><td>Yes</td><td>Events in the order they are generated (details below)</td></tr></tbody></table>

### Response <a href="#response" id="response"></a>

The server responds with code 200 if everything is OK.

<table><thead><tr><th width="163">HTTP Code</th><th>State</th></tr></thead><tbody><tr><td>413</td><td>Incorrect size of the data packet (exceeds the maximum)</td></tr><tr><td>400</td><td>devtodev App ID is absent</td></tr><tr><td>403</td><td>Incorrect devtodev App ID</td></tr><tr><td>403</td><td>Administrative restrictions on data received from a client</td></tr><tr><td>400</td><td><p>The error of data unpacking</p><p><code>{</code></p><p><code>"error_message":"Wrong GZIP format"</code></p><p><code>}</code></p></td></tr><tr><td>400</td><td><p>The error of JSON format</p><p><code>{</code></p><p><code>"error_message":"Wrong JSON format"</code></p><p><code>}</code></p></td></tr><tr><td>200</td><td>OK. Data received</td></tr></tbody></table>

## Service Events <a href="#events" id="events"></a>

### Device Info <a href="#device-info" id="device-info"></a>

Contains information about the user's device.&#x20;

{% hint style="warning" %}
You must send the Device Info event as the first event when a new user is created. Without it, the user will not be registered in the system!&#x20;
{% endhint %}

It is also desirable to send this event at the beginning of each session.&#x20;

<table><thead><tr><th width="183">Parameter</th><th width="107">Platform</th><th width="104">Required</th><th width="89">Type</th><th>Description</th></tr></thead><tbody><tr><td>code</td><td>Any</td><td>Yes</td><td>String</td><td>The unique ID of the event. Takes the value "<strong>di</strong>"</td></tr><tr><td>timestamp</td><td>Any</td><td>Yes</td><td>Long</td><td>The date the event was generated. Unix timestamp in milliseconds</td></tr><tr><td>osVersion</td><td>Any</td><td>No</td><td>String</td><td>Operating system version</td></tr><tr><td>os</td><td>Any</td><td>No</td><td>String</td><td>OS family name (Android, iOS, Mac, Windows…)</td></tr><tr><td>displayPpi</td><td>Any</td><td>No</td><td>Int</td><td>Screen pixel density</td></tr><tr><td>displayResolution</td><td>Any</td><td>No</td><td>String</td><td>Screen resolution in pixels, sent as "longSide"x"shortSide" ("1024x768")</td></tr><tr><td>displayDiagonal</td><td>Any</td><td>No</td><td>Double</td><td>Screen size (inches)</td></tr><tr><td>manufacturer</td><td>Android, iOS, Mac</td><td>No</td><td>String</td><td>Device manufacturer (Apple, Samsung)</td></tr><tr><td>model</td><td>Android, iOS, Mac</td><td>No</td><td>String</td><td>Device model. Value of the name (iOS), model (Android)</td></tr><tr><td>deviceType</td><td>Any</td><td>No</td><td>Int</td><td>Device Type:<br>0 - Unknown<br>1 - Phone<br>2 - Tablet<br>3 - Desktop<br>4 - Watch<br>5 - TV<br>6 - Simulator</td></tr><tr><td>timeZoneOffset</td><td>Any</td><td>No</td><td>Int</td><td>The UTC offset (or time offset) in seconds</td></tr><tr><td>rooted</td><td>Any</td><td>No</td><td>Bool</td><td>Is the device rooted</td></tr><tr><td>isLimitAdTrackingEnabled</td><td>iOS, Android, Windows</td><td>No</td><td>Bool</td><td>Whether ad tracking restriction is enabled</td></tr><tr><td>userAgent</td><td>Any</td><td>No</td><td>String</td><td>User-Agent header</td></tr><tr><td>idfv</td><td>iOS, Mac</td><td>No</td><td>String</td><td>identifierForVendor</td></tr><tr><td>idfa</td><td>iOS, Mac</td><td>No</td><td>String</td><td>advertisingIdentifier</td></tr><tr><td>androidId</td><td>Android</td><td>No</td><td>String</td><td>Device SSAID. Not recommended for use</td></tr><tr><td>advertisingId</td><td>Android, Windows</td><td>No</td><td>String</td><td>Advertising ID</td></tr><tr><td>serialId</td><td>Android, Windows</td><td>No</td><td>String</td><td>Build.Serial (Android). Not recommended for use.</td></tr><tr><td>uuid</td><td><p>Android, Windows,</p><p>iOS,<br>macOS</p></td><td>No</td><td>String</td><td><p>A unique device identifier in UUID format (RFC 4122). <br><a href="http://www.ietf.org/rfc/rfc4122.txt">RFC 4122</a></p><p><a href="https://developer.apple.com/documentation/corefoundation/cfuuid?language=objc">CFUUID | Apple Developer Documentation</a></p></td></tr><tr><td>instanceId</td><td>Android</td><td>No</td><td>String</td><td>InstanceId.Get();</td></tr><tr><td>sandboxState</td><td>iOS</td><td>No</td><td>Int</td><td>Information about how the application is built: undefined (0) / sandbox (1) / production (2)</td></tr></tbody></table>

Example

```json
{
	"code": "di",
	"timestamp": 1694783505123,
	"osVersion": "10.2.2",
	"os": "iOS",
	"displayPpi": 401,
	"displayResolution": "1920x1080",
	"displayDiagonal": 5.5,
	"manufacturer": "Apple",
	"model": "iPhone8,2",
	"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/602.3.12 (KHTML, like Gecko) Version/10.0.2 Safari/602.3.12",
	"timeZoneOffset": 7200,
	"idfv": "30FE1CE1-1125-4657-97B0-638744C3C6D1",
	"idfa": "0A60DCF2-3186-4801-9192-D8CFA995DD6D"
}
```

### Session Start <a href="#session-start" id="session-start"></a>

Sends information about the start of a new session.&#x20;

{% hint style="warning" %}
In order to [track sessions](/integration/integration-of-sdk-v2/setting-up-events/track-sessions) fully, you also need to send a [User Engagement](#user-engagement) event.
{% endhint %}

<table><thead><tr><th width="142">Parameter</th><th width="106">Required</th><th width="83">Type</th><th>Description</th></tr></thead><tbody><tr><td>code</td><td>Yes</td><td>String</td><td>The unique ID of the event. Takes the value "<strong>ss"</strong></td></tr><tr><td>timestamp</td><td>Yes</td><td>Long</td><td>The date of the new session start. Unix timestamp in milliseconds</td></tr><tr><td>level</td><td>Yes</td><td>Int</td><td>User (player) level at the time the event was generated. For non-gaming applications, set this to 1.</td></tr><tr><td>sessionId</td><td>No</td><td>Long</td><td>The ID of the current session. This is a kind of key by which you can link all user events that occurred during one session. This can be a session start timestamp, or you can use some custom numeric ID, or the user's session sequence number if you have that data.</td></tr></tbody></table>

Example

```json
{
    "code": "ss",
    "timestamp": 1694783505123,
    "level": 1
}
```

### User Engagement <a href="#user-engagement" id="user-engagement"></a>

{% hint style="warning" %}
Required for proper [Session](#session-start) tracking.
{% endhint %}

Sends information about the duration of user activity. This can be either the full length of the session or a part of the user's session while the application was in focus.

<table><thead><tr><th width="139">Parameter</th><th width="106">Required</th><th width="91">Type</th><th>Description</th></tr></thead><tbody><tr><td>code</td><td>Yes</td><td>String</td><td>The unique ID of the event. Takes the value "<strong>ue"</strong></td></tr><tr><td>timestamp</td><td>Yes</td><td>Long</td><td>The date the event was generated. Unix timestamp in milliseconds</td></tr><tr><td>level</td><td>Yes</td><td>Int</td><td>User (player) level at the time the event was generated. For non-gaming applications, set this to 1.</td></tr><tr><td>length</td><td>Yes</td><td>Int</td><td>Full session duration or a part of a user session in seconds</td></tr><tr><td>sessionId</td><td>No</td><td>Long</td><td>The ID of the current <a href="#session-start">session</a></td></tr></tbody></table>

Example

```json
{
    "code": "ue",
    "timestamp": 1694783735122,
    "level": 1,
    "length": 230,
    "sessionId": 1694783505123
}
```

### Setting User Tracking Status (GDPR) <a href="#setting-user-tracking-status-gdpr" id="setting-user-tracking-status-gdpr"></a>

This event denies/allows tracking of user data and also implements the right to be forgotten in accordance with the requirements of the GDPR.

A developer must use this event in case a user doesn’t want their data to be sent and processed in the devtodev system.

When calling the event with the parameter "trackingAllowed"*: false*, it is a command to the server to delete all user’s personal data that has been collected by devtodev from this app and a command to block the collection of any data of this user in future.

The user will remain in the devtodev system only as an impersonal unit in the previously aggregated metrics.

If it is set to “***true***”, tracking can be enabled again. In this case, the user will be considered new.

<table><thead><tr><th width="173">Parameter</th><th width="107">Required</th><th width="92">Type</th><th>Description</th></tr></thead><tbody><tr><td>code</td><td>Yes</td><td>String</td><td>The unique ID of the event. Takes the value <strong>"ts"</strong></td></tr><tr><td>trackingAllowed</td><td>Yes</td><td>Bool</td><td>Enable (true) or disable (false) user tracking</td></tr><tr><td>timestamp</td><td>Yes</td><td>Long</td><td>The date the event was generated. Unix timestamp in milliseconds</td></tr></tbody></table>

Example

```json
{
    "code": "ts",
    "trackingAllowed": false,
    "timestamp": 1694783565763
}
```

### Alive <a href="#alive" id="alive"></a>

Service event. Not obligatory. Ping event for the server. It is required to correctly display a player online if more than 5 minutes have passed since his last sent event.

<table><thead><tr><th width="139">Parameter</th><th width="104">Required</th><th width="88">Type</th><th>Description</th></tr></thead><tbody><tr><td>code</td><td>Yes</td><td>String</td><td>The unique ID of the event. Takes the value <strong>"al"</strong></td></tr><tr><td>timestamp</td><td>Yes</td><td>Long</td><td>The date the event was generated. Unix timestamp in milliseconds</td></tr><tr><td>sessionId</td><td>No</td><td>Long</td><td>The ID of the current <a href="#session-start">session</a></td></tr></tbody></table>

Example

```json
{
    "code": "al",
    "timestamp": 1694783607178
}
```

## User properties <a href="#a-d-impression" id="a-d-impression"></a>

### People

Each devtodev project can have **up to 30 custom user properties.**

{% hint style="warning" %}
Attention! We strongly recommend that you do not use these properties to transfer and store data that fits the definition of [personal data](https://gdpr-info.eu/issues/personal-data/)!
{% endhint %}

<table><thead><tr><th width="135">Parameter</th><th width="107">Required</th><th width="129">Type</th><th>Description</th></tr></thead><tbody><tr><td>code</td><td>Yes</td><td>String</td><td>The unique ID of the event. Takes the value <strong>"pl"</strong></td></tr><tr><td>timestamp</td><td>Yes</td><td>Long</td><td>The date the event was generated. Unix timestamp in milliseconds</td></tr><tr><td>level</td><td>Yes</td><td>Int</td><td>User (player) level at the time the event was generated. For non-gaming applications, set this to 1.</td></tr><tr><td>parameters</td><td>Yes</td><td>Object&#x3C;Key, Value></td><td><p>User characteristics in key-value format. May contain predefined (tester, cheater, name, email, phone, photo, gender, age) and custom user propery names.</p><p>User custom property values can be a number, a string (up to 500 symbols), or a boolean value</p></td></tr><tr><td>sessionId</td><td>No</td><td>Long</td><td>The ID of the current <a href="#session-start">session</a></td></tr></tbody></table>

Predefined user properties (in addition to 30 custom user properties).

<table><thead><tr><th width="132">Property</th><th width="112">Required</th><th width="128">Type</th><th>Description</th></tr></thead><tbody><tr><td>tester</td><td>No</td><td>bool</td><td>Set true if user is a tester. This user's data will not be used to calculate metrics.</td></tr><tr><td>cheater</td><td>No</td><td>bool</td><td>Set true if user is a cheater. This user's data will not be used to calculate metrics.</td></tr></tbody></table>

{% hint style="warning" %}
If you need to exclude cheaters/testers transactions from statistics, go to **Users & Segments** section in devtodev interface and [mark the user](/reports-and-functionality/project-related-reports-and-fuctionality/users#mark-as-cheater-or-tester) manually. \
When you mark the user in devtodev interface, the system removes their transactions for the last 7 days the reports and recalculates the metrics.&#x20;
{% endhint %}

Example

```json
{
	"code": "pl",
	"timestamp": 1694783511089,
	"sessionId": 1694783505123,
	"level": 2,
	"parameters": {
		"name": "John Doe",
		"cheater": false,
		"age": 21
	}
}
```

## Basic Events <a href="#currency-accrual" id="currency-accrual"></a>

### Custom Event <a href="#custom-event" id="custom-event"></a>

If you want to track non-basic events (below), you can create custom events of your own. How you are going to apply them depends solely on you.

{% hint style="info" %}
devtodev supports 300 custom event names in a single project (depends on the [pricing plan](https://www.devtodev.com/pricing)). Events that exceed the limit of custom event names will be discarded. Try to integrate the tracked actions by type to the event name level, and move the characteristic tags to the parameters.

*For example, if you need to track purchases of “Paper” and “Pen” items, you don’t need to create two events with the names “Paper Purchase” and “Pen Purchase”. Instead, create a single “Purchase” event and add an “Item” parameter with the appropriate value of “Paper” or “Pen”. This way, you can use just one event to track many items.*

For a string parameter, you can use no more than 50,000 unique values ​​for the entire history of events. If the number of unique values exceeds the limit, the parameter gets locked by the system and is discarded from the received data. Therefore, we don’t recommend using highly variable parameters like user IDs or time as string values ​​(moreover, they are automatically added to the event).

We strongly recommend that you do not change the data type passed in the same parameter. If you change the data type in a parameter, it will be duplicated with the same name, which may cause issues while processing reports.
{% endhint %}

{% hint style="warning" %}
We strongly recommend that you do not use custom event properties to transfer and store data that fits the definition of [personal data](https://gdpr-info.eu/issues/personal-data/)!
{% endhint %}

<table><thead><tr><th width="143">Parameter</th><th width="108">Required</th><th width="128">Type</th><th>Description</th></tr></thead><tbody><tr><td>code</td><td>Yes</td><td>String</td><td>The unique ID of the event. Takes the value <strong>"ce"</strong></td></tr><tr><td>timestamp</td><td>Yes</td><td>Long</td><td>The date the event was generated. Unix timestamp in milliseconds</td></tr><tr><td>level</td><td>Yes</td><td>Int</td><td>User (player) level at the time the event was generated. For non-gaming applications, set this to 1.</td></tr><tr><td>name</td><td>Yes</td><td>String (72)</td><td>Custom event name</td></tr><tr><td>parameters</td><td>No</td><td>Object&#x3C;Key (32),Value></td><td>Custom event parameters. The number of custom event parameters allowed depends on your <a href="/pages/-Lo5-65RSa0PmW6UWJN5">plan</a>.<br>The string value must not exceed 255 characters.</td></tr><tr><td>sessionId</td><td>No</td><td>Long</td><td>The ID of the current <a href="#session-start">session</a></td></tr></tbody></table>

Example&#x20;

```json
{
	"code": "ce",
	"timestamp": 1694783521034,
	"sessionId": 1694783505123,
	"level": 2,
	"name": "eventName",
	"parameters": {
		"intParameter": 134,
		"stringParameter": "hello",  //255 symbols max
		"doubleParameter": 12.98
	}
}
```

### Real Payment  <a href="#real-currency-payment" id="real-currency-payment"></a>

To track payments in a real currency, dispatch this event right after the system validates that the payment went through successfully. The event is fundamental and mandatory for all the app metrics related to monetization.

{% hint style="info" %}
By default (easy to change in the app’s settings), the devtodev server invalidates transactions with previously-used identifiers. Additionally, the server performs identifier checks based on their outer appearance in order to avoid obvious fraud.
{% endhint %}

<table><thead><tr><th width="159">Parameter</th><th width="104">Required</th><th width="124">Type</th><th>Description</th></tr></thead><tbody><tr><td>code</td><td>Yes</td><td>String</td><td>The unique ID of the event. Takes the value <strong>"rp"</strong></td></tr><tr><td>timestamp</td><td>Yes</td><td>Long</td><td>The date the event was generated. Unix timestamp in milliseconds</td></tr><tr><td>level</td><td>Yes</td><td>Int</td><td>User (player) level at the time the event was generated. For non-gaming applications, set this to 1.</td></tr><tr><td>productId</td><td>Yes</td><td>String (255)</td><td>Product name. We recommend using the product SKU</td></tr><tr><td>orderId</td><td>Yes</td><td>String (64)</td><td>Unique transaction ID</td></tr><tr><td>price</td><td>Yes</td><td>Double</td><td>Price in user currency</td></tr><tr><td>currencyCode</td><td>Yes</td><td>String (3)</td><td>ISO 4217 alphabetic code of the user's currency</td></tr><tr><td>sessionId</td><td>No</td><td>Long</td><td>The ID of the current <a href="#session-start">session</a></td></tr></tbody></table>

Example

```json
{
	"code": "rp",
	"timestamp": 1694783605101,
	"sessionId": 1694783505123,
	"level": 2,
	"productId": "starter_pack",
	"orderId": "GPA.100054271428",
	"price": 19.99,
	"currencyCode": "USD"
}
```

### Onboarding (tutorial)

Tracks the progress of the user's initial training (tutorial). The event is used to build a funnel through which users go through learning stages (steps). Each step is represented by an integer greater than 0.\
If you plan to add additional intermediate learning steps, you can number the steps initially, for example, in increments of 10.\
Additionally, three constants are used to describe the start of training, successful completion of training, and skipping the entire training process.

Recommended sequence for dispatching events:

1. Training started (**-1**).
2. Sequential sending of events at the entrance to the next learning step (**1...N**).
3. Completion of training after passing the last step of training (**-2**).

The tutorial skip logic assumes that a single event with a value of **0** is sent, replacing the entire sequence described above.

<table><thead><tr><th width="131">Parameter</th><th width="105">Required</th><th width="88">Type</th><th>Description</th></tr></thead><tbody><tr><td>code</td><td>Yes</td><td>String</td><td>The unique ID of the event. Takes the value <strong>"tr"</strong></td></tr><tr><td>timestamp</td><td>Yes</td><td>Long</td><td>The date the event was generated. Unix timestamp in milliseconds</td></tr><tr><td>level</td><td>Yes</td><td>Int</td><td>User (player) level at the time the event was generated. For non-gaming applications, set this to 1.</td></tr><tr><td>step</td><td>Yes</td><td>Int</td><td>Tutorial step number or predefined values (0, -1, -2 see above)</td></tr><tr><td>sessionId</td><td>No</td><td>Long</td><td>The ID of the current <a href="#session-start">session</a></td></tr></tbody></table>

Example

```json
{
	"code": "tr",
	"timestamp": 1694783715371,
	"sessionId": 1694783505123,
	"level": 2,
	"step": -1
}
```

### Virtual Currency Payment  <a href="#virtual-currency-payment" id="virtual-currency-payment"></a>

This event is for games only.

Pass this event after every purchase if you want to track in-app (virtual) currency spends and items’ popularity.

<table><thead><tr><th width="182">Parameter</th><th width="104">Required</th><th width="147">Type</th><th>Description</th></tr></thead><tbody><tr><td>code</td><td>Yes</td><td>String</td><td>The unique ID of the event. Takes the value <strong>"vp"</strong> ("ip" in the previous version of the API)</td></tr><tr><td>timestamp</td><td>Yes</td><td>Long</td><td>The date the event was generated. Unix timestamp in milliseconds</td></tr><tr><td>level</td><td>Yes</td><td>Int</td><td>User (player) level at the time the event was generated</td></tr><tr><td>purchaseAmount</td><td>Yes</td><td>Int</td><td>Number of items purchased</td></tr><tr><td>purchasePrice</td><td>Yes</td><td>Object&#x3C;String (24), Number></td><td>Currency and price of the purchased items (or currencies, if there are multiple)</td></tr><tr><td>purchaseType</td><td>Yes</td><td>String (96)</td><td>The group to which the item belongs</td></tr><tr><td>purchaseId</td><td>Yes</td><td>String (32)</td><td>Item ID or name</td></tr><tr><td>sessionId</td><td>No</td><td>Long</td><td>The ID of the current <a href="#session-start">session</a></td></tr></tbody></table>

Example

```json
{
	"code": "vp",
	"timestamp": 1694783555833,
	"sessionId": 1694783505123,
	"level": 2,
	"purchaseId": "house_327",
	"purchaseAmount": 2,
	"purchasePrice": {
		"coins": 500,
		"wood": 2
	},
	"purchaseType": "buildings"
}
```

### Currency Accrual <a href="#currency-accrual" id="currency-accrual"></a>

This event is for games only.

The event involves the accumulation of in-game currency or resources. It contains data on the amount of in-game currency earned or purchased by the user. It is highly undesirable to send this data transactionally. Instead, please send aggregated data for a specific period, such as 5-10 minutes. Additionally, the accumulation of data must be interrupted, and an event should be sent if the user's level has changed.

{% hint style="warning" %}
Attention! The total number of tracked unique resources (virtual currencies) cannot exceed 30 items throughout the project's lifespan.
{% endhint %}

Furthermore, apart from grouping by type (earned/purchased), there is also a grouping by the source of income.

<table><thead><tr><th width="132">Parameter</th><th width="150">Required</th><th width="185">Type</th><th>Description</th></tr></thead><tbody><tr><td>code</td><td>Yes</td><td>String</td><td>The unique ID of the event. Takes the value <strong>"ca"</strong></td></tr><tr><td>timestamp</td><td>Yes</td><td>Long</td><td>The date the event was generated. Unix timestamp in milliseconds</td></tr><tr><td>level</td><td>Yes</td><td>Int</td><td>User (player) level at the time the event was generated</td></tr><tr><td>bought</td><td>Yes<br>one or both (bought and earned)</td><td>Object&#x3C;String, Object&#x3C;String (24), Number>></td><td>Resources of the type "bought" (purchased) aggregated over a specific period by source and resource name</td></tr><tr><td>earned</td><td>Yes<br>one or both (bought and earned)</td><td>Object&#x3C;String, Object&#x3C;String (24), Number>></td><td>Resources of type "earned" aggregated over a specific period by source and resource name</td></tr><tr><td>sessionId</td><td>No</td><td>Long</td><td>The ID of the current <a href="#session-start">session</a></td></tr></tbody></table>

```json
{
	"code": "ca",
	"timestamp": 1694783625364,
	"sessionId": 1694783505123,
	"level": 2,
	"bought": {
		"sourceA": {
			"resource1": 1231,
			"resource2": 1231
		},
		"sourceB": {
			"resource1": 12,
			"resource2": 31
		},
		"sourceC": {
			"resource2": 40,
			"resource1": 10
		}
	},
	"earned": {
		"sourceA": {
			"resource1": 1231,
			"resource2": 1231
		},
		"sourceD": {
			"resource1": 1231,
			"resource2": 1231
		}
	}
}
```

### Current Balance

This event is for games only.

This event is used to generate a preset game report called Economy Balance (currency balance with grouping by days). This report shows the approximate amount of virtual currency on users' hands, which is useful for planning and checking the results of campaigns aimed at removing excess virtual currency from the application.

{% hint style="warning" %}
This event should not be sent more than once per day for a user.
{% endhint %}

<table><thead><tr><th width="159">Parameter</th><th width="102">Required</th><th width="153">Type</th><th>Description</th></tr></thead><tbody><tr><td>code</td><td>Yes</td><td>String</td><td>The unique ID of the event. Takes the value <strong>"cb"</strong></td></tr><tr><td>balance</td><td>Yes</td><td>Object&#x3C;String(24),Number></td><td>User's balances of virtual currencies or resources at the time the event was generated</td></tr><tr><td>timestamp</td><td>Yes</td><td>Long</td><td>The date the event was generated. Unix timestamp in milliseconds</td></tr><tr><td>level</td><td>Yes</td><td>Int</td><td>User (player) level at the time the event was generated</td></tr><tr><td>sessionId</td><td>No</td><td>Long</td><td>The ID of the current <a href="#session-start">session</a></td></tr></tbody></table>

Example

```json
{
	"code": "cb",
	"timestamp": 1694783615284,
	"sessionId": 1694783505123,
	"level": 2,
	"balance": {
		"money1": 123,
		"money2": 11
	}
}
```

### Level Up <a href="#level-up" id="level-up"></a>

This event is for games only.

Leveling up the user (player). The event is triggered when the user reaches a new level. In addition o the achieved level number, you can also include data on the balance of virtual currencies or resources at the time the new level was reached, as well as the total amount of currencies or resources spent, earned and purchased by the user during the previous level progression.

{% hint style="warning" %}
Attention! The total number of tracked unique resources (virtual currencies) cannot exceed 30 items throughout the entire life of the project.
{% endhint %}

<table><thead><tr><th width="135">Parameter</th><th width="106">Required</th><th width="152">Type</th><th>Description</th></tr></thead><tbody><tr><td>code</td><td>Yes</td><td>String</td><td>The unique ID of the event. Takes the value <strong>"lu"</strong></td></tr><tr><td>timestamp</td><td>Yes</td><td>Long</td><td>The date the event was generated. Unix timestamp in milliseconds</td></tr><tr><td>level</td><td>Yes</td><td>Int</td><td>User (player) level achieved by the user</td></tr><tr><td>balance</td><td>No</td><td>Object&#x3C;String (24), Number></td><td>User's balances of virtual currencies or resources at the time of leveling up</td></tr><tr><td>spent</td><td>No</td><td>Object&#x3C;String (24), Number></td><td><p>Total expenses of resources (virtual currency) by the user during level progression</p><p>In the SDK, this data is aggregated from virtual currency payment events</p></td></tr><tr><td>earned</td><td>No</td><td>Object&#x3C;String (24), Number></td><td><p>The total amount of resources (virtual currency) earned by the user during level progression</p><p>In the SDK, this data is aggregated from CurrencyAccrual events with the "earned" type</p></td></tr><tr><td>bought</td><td>No</td><td>Object&#x3C;String (24), Number></td><td><p>The total number of resources (virtual currency) purchased by the user for real currency during level progression</p><p>In the SDK, data is aggregated from the CurrencyAccrual events with the "bought" type</p></td></tr><tr><td>sessionId</td><td>No</td><td>Long</td><td>The ID of the current <a href="#session-start">session</a></td></tr></tbody></table>

Example

```json
{
	"code": "lu",
	"timestamp": 1694783675815,
	"sessionId": 1694783505123,
	"level": 2,
	"balance": {
		"money1": 123,
		"money2": 11
	},
	"spent": {
		"money1": 12,
		"money2": 2,
		"wood": 12
	},
	"earned": {
		"crystals": 5
	},
	"bought": {
		"wood": 200
	}
}
```

### Progression Event <a href="#progression-event" id="progression-event"></a>

This event is for games only.

First of all, the progression event is used in games with short (within one game session) areas/game levels, for example, match 3 games. You can use the event to collect data on how well or how fast users complete levels, how difficult it is for them, how many resources they gained or spent, and other relevant parameters.

<table><thead><tr><th width="135">Parameter</th><th width="107">Required</th><th width="179">Type</th><th>Description</th></tr></thead><tbody><tr><td>code</td><td>Yes</td><td>String</td><td>The unique ID of the event. Takes the value <strong>"pe"</strong></td></tr><tr><td>timestamp</td><td>Yes</td><td>Long</td><td>The date the event was generated. Unix timestamp in milliseconds</td></tr><tr><td>level</td><td>Yes</td><td>Int</td><td>User (player) level at the time the event was generated</td></tr><tr><td>parameters</td><td>Yes</td><td>Object</td><td>Event parameters. See below</td></tr><tr><td>spent</td><td>No</td><td>Object&#x3C;String (24), Number></td><td>Resources consumed during an area completion</td></tr><tr><td>earned</td><td>No</td><td>Object&#x3C;String (24), Number></td><td>Resources earned during an area completion.</td></tr><tr><td>name</td><td>Yes</td><td>String (40)</td><td>The name of the event. It is usually the number or the name of the area/location/level.</td></tr><tr><td>sessionId</td><td>No</td><td>Long</td><td>The ID of the current <a href="#session-start">session</a></td></tr></tbody></table>

parameters object:

<table><thead><tr><th width="132">Parameter</th><th width="110">Required</th><th width="120">Type</th><th>Description</th></tr></thead><tbody><tr><td>difficulty</td><td>No</td><td>Int</td><td>An optional difficulty value</td></tr><tr><td>source</td><td>No</td><td>String (40)</td><td>The name of the previous progression event used for connecting events together. E.g. a previous area visited by the player</td></tr><tr><td>success</td><td>Yes</td><td>Bool</td><td>The completion event result: “true” if successful, “false” if unsuccessful/lost</td></tr><tr><td>duration</td><td>Yes</td><td>Long</td><td>Time in seconds taken to complete the area</td></tr></tbody></table>

Example

```json
{
	"code": "pe",
	"timestamp": 1694783885625,
	"sessionId": 1694783505123,
	"level": 2,
	"name": "MyAwesomeLocation",
	"parameters": {
		"source": "location1",
		"difficulty": 1,
		"success": true,
		"duration": 180
	},
	"spent": {
		"money1": 12,
		"money2": 2,
		"wood": 12
	},
	"earned": {
		"money1": 8,
		"money2": 2,
		"stone": 1
	}
}
```

All events generated during the passage of the location are recommended to be marked with the key "inProgress," the value of which indicates the name of the location (Progression event name).

Example

```json
{
	"code": "rp",
	"timestamp": 1694783895114,
	"sessionId": 1694783505123,
	"level": 2,
	"productId": "starter_pack",
	"orderId": "GPA.100054271428",
	"price": 19.99,
	"currencyCode": "USD",
	"inProgress": ["MyAwesomeLocation"]
}
```

## Secondary Events <a href="#a-d-impression" id="a-d-impression"></a>

### Referral <a href="#referral" id="referral"></a>

Tracking the source of the application installation. Sent once per user. Does not need to be sent if ad trackers such as AppsFlyer are integrated in the project settings or the devtodev custom postback API is used.

<table><thead><tr><th width="134">Parameter</th><th width="106">Required</th><th width="89">Type</th><th>Description</th></tr></thead><tbody><tr><td>code</td><td>Yes</td><td>String</td><td>The unique ID of the event. Takes the value <strong>"rf"</strong></td></tr><tr><td>timestamp</td><td>Yes</td><td>Long</td><td>The date the event was generated. Unix timestamp in milliseconds</td></tr><tr><td>source</td><td>No</td><td>String</td><td>Ad network name</td></tr><tr><td>campaign</td><td>No</td><td>String</td><td>Ad campaign name</td></tr><tr><td>content</td><td>No</td><td>String</td><td>Campaign content (for example, for A/B testing of site elements or contextual ads)</td></tr><tr><td>medium</td><td>No</td><td>String</td><td>Traffic type</td></tr><tr><td>term</td><td>No</td><td>String</td><td>Campaign search term (for example, PPC keywords)</td></tr><tr><td>sessionId</td><td>No</td><td>Long</td><td>The ID of the current <a href="#session-start">session</a></td></tr></tbody></table>

Example

```json
{
	"code": "rf",
	"timestamp": 1694783915948,
	"sessionId": 1694783505123,
	"source": "google",
	"campaign": "christmas",
	"content": "sale for ducks",
	"medium": "traff",
	"term": "a,b,c,d,e"
}
```

### Ad Impression <a href="#a-d-impression" id="a-d-impression"></a>

The event is used for individual tracking of ad revenue on user devices. This method is used if there are impression revenue data available on the client device (they can be obtained from the ad network SDK).

{% hint style="info" %}
Do not apply this event if you use ad networks that utilize the server-server protocol for sending ad revenue data (ironSource, AppLovin MAX, and Fyber networks) and you already set up this method of data collection because if you use both data sources, your revenue data may be duplicated.
{% endhint %}

<table><thead><tr><th width="140">Parameter</th><th width="105">Required</th><th width="124">Type</th><th>Description</th></tr></thead><tbody><tr><td>code</td><td>Yes</td><td>String</td><td>The unique ID of the event. Takes the value <strong>"adrv"</strong></td></tr><tr><td>timestamp</td><td>Yes</td><td>Long</td><td>The date the event was generated. Unix timestamp in milliseconds</td></tr><tr><td>source</td><td>No</td><td>String (100)</td><td>Impression data source name</td></tr><tr><td>ad_network</td><td>Yes</td><td>String (100)</td><td>The name of the ad network that delivered the impression</td></tr><tr><td>placement</td><td>No</td><td>String (100)</td><td>Placement of the banner</td></tr><tr><td>ad_unit</td><td>No</td><td>String (100)</td><td>Banner title</td></tr><tr><td>revenue</td><td>Yes</td><td>Double</td><td>Reward for displaying a banner in USD</td></tr><tr><td>sessionId</td><td>No</td><td>Long</td><td>The ID of the current <a href="#session-start">session</a></td></tr></tbody></table>

Example

```json
{
	"code": "adrv",
	"timestamp": 1694783775187,
	"sessionId": 1694783505123,
	"source": "IronSource",
	"ad_network": "Facebook",
	"placement": "End of the round",
	"ad_unit": "TestAdUnit",
	"revenue": 0.3434
}
```

### Social Connect <a href="#social-connect" id="social-connect"></a>

The event tracks the user's connection to a social network. It is sent at the moment when the application receives information about the successful authorization of the user on the social network.

<table><thead><tr><th width="163">Parameter</th><th width="111">Required</th><th width="86">Type</th><th>Description</th></tr></thead><tbody><tr><td>code</td><td>Yes</td><td>String</td><td>The unique ID of the event. Takes the value <strong>"sc"</strong></td></tr><tr><td>timestamp</td><td>Yes</td><td>Long</td><td>The date the event was generated. Unix timestamp in milliseconds</td></tr><tr><td>level</td><td>Yes</td><td>Int</td><td>User (player) level at the time the event was generated. For non-gaming applications, set this to 1.</td></tr><tr><td>socialNetwork</td><td>Yes</td><td>String</td><td>Code or name of the social network. May contain predefined values (see below)</td></tr><tr><td>sessionId</td><td>No</td><td>Long</td><td>The ID of the current <a href="#session-start">session</a></td></tr></tbody></table>

Example

```json
{
	"code": "sc",
	"timestamp": 1694783815155,
	"sessionId": 1694783505123,
	"level": 2,
	"socialNetwork": "fb"
}
```

**Preset social network codes**

<table><thead><tr><th width="193">Social Network</th><th>Code</th></tr></thead><tbody><tr><td>Vkontakte</td><td>vk</td></tr><tr><td>X (formerly Twitter)</td><td>tw</td></tr><tr><td>Facebook</td><td>fb</td></tr><tr><td>WhatsApp</td><td>wp</td></tr><tr><td>Viber</td><td>vb</td></tr><tr><td>Evernote</td><td>en</td></tr><tr><td>Google Mail</td><td>gm</td></tr><tr><td>LinkedIn</td><td>in</td></tr><tr><td>Pinterest</td><td>pi</td></tr><tr><td>Qzone</td><td>qq</td></tr><tr><td>Reddit</td><td>rt</td></tr><tr><td>Renren</td><td>rr</td></tr><tr><td>Tumblr</td><td>tb</td></tr></tbody></table>

### Social Post <a href="#social-post" id="social-post"></a>

The event tracks the user's posts on the social network. It is dispatched when a success report is received from the network, if possible.

<table><thead><tr><th width="165">Parameter</th><th width="107">Required</th><th width="89">Type</th><th>Description</th></tr></thead><tbody><tr><td>code</td><td>Yes</td><td>String</td><td>The unique ID of the event. Takes the value <strong>"sp"</strong></td></tr><tr><td>timestamp</td><td>Yes</td><td>Long</td><td>The date the event was generated. Unix timestamp in milliseconds</td></tr><tr><td>level</td><td>Yes</td><td>Int</td><td>User (player) level at the time the event was generated. For non-gaming applications, set this to 1.</td></tr><tr><td>socialNetwork</td><td>Yes</td><td>String</td><td>Code or name of the social network. May contain predefined values (see above)</td></tr><tr><td>postReason</td><td>Yes</td><td>String</td><td>Reason for posting</td></tr><tr><td>sessionId</td><td>No</td><td>Long</td><td>The ID of the current <a href="#session-start">session</a></td></tr></tbody></table>

Example

```json
{
	"code": "sp",
	"timestamp": 1694783905478,
	"sessionId": 1694783505123,
	"level": 2,
	"socialNetwork": "fb",
	"postReason": "quest_completed"
}
```

## Example <a href="#real-currency-payment" id="real-currency-payment"></a>

An example of a package describing a single session of a single user.

{% hint style="info" %}
Note: devtodev does not accept data that is older than 7 days and data that is more than 3 hours in the future. If you want to use this example, you will need to edit the **`timestamp`** parameters.
{% endhint %}

{% code fullWidth="true" %}

```json
{
	"reports": [{
		"deviceId": "0A60DCF2-3186-4801-9192-D8CFA995DD6D",
		"userId": "u_100500",
		"packages": [{
			"language": "en",
			"country": "US",
			"appVersion": "1.0",
			"events": [{
					"code": "di",
					"osVersion": "10.2.2",
					"os": "iOS",
					"displayPpi": 401,
					"displayResolution": "1920x1080",
					"displayDiagonal": 5.5,
					"manufacturer": "Apple",
					"model": "iPhone8,2",
					"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/602.3.12 (KHTML, like Gecko) Version/10.0.2 Safari/602.3.12",
					"timeZoneOffset": 7200,
					"idfv": "30FE1CE1-1125-4657-97B0-638744C3C6D1",
					"idfa": "0A60DCF2-3186-4801-9192-D8CFA995DD6D",
					"timestamp": 1694783505122
				}, {
					"code": "ss",
					"timestamp": 1694783505123,
					"level": 1
				}, {
					"code": "pl",
					"timestamp": 1694783511089,
					"sessionId": 1694783505123,
					"level": 1,
					"parameters": {
						"name": "John Doe",
						"cheater": false,
						"age": 21
					}
				}, {
					"code": "tr",
					"timestamp": 1694783715371,
					"sessionId": 1694783505123,
					"level": 1,
					"step": -1
				}, {
					"code": "tr",
					"timestamp": 1694783725344,
					"sessionId": 1694783505123,
					"level": 1,
					"step": 1
				}, {
					"code": "lu",
					"timestamp": 1694783736345,
					"sessionId": 1694783505123,
					"level": 2,
					"balance": {
						"money1": 123,
						"money2": 11
					}
				}, {
					"code": "tr",
					"timestamp": 1694783741456,
					"sessionId": 1694783505123,
					"level": 2,
					"step": 2
				}, {
					"code": "tr",
					"timestamp": 1694783752425,
					"sessionId": 1694783505123,
					"level": 2,
					"step": -2
				}, {
					"code": "ce",
					"timestamp": 1694783773675,
					"sessionId": 1694783505123,
					"level": 2,
					"name": "eventName",
					"parameters": {
						"intParameter": 134,
						"stringParameter": "hello",
						"doubleParameter": 12.98
					}
				},
				{
					"code": "rp",
					"timestamp": 1694783798278,
					"sessionId": 1694783505123,
					"level": 2,
					"productId": "com.example.application.starterpack",
					"orderId": "280001601071201",
					"price": 19.99,
					"currencyCode": "USD"

				}, {
					"code": "ue",
					"timestamp": 1694783898278,
					"level": 2,
					"length": 393,
					"sessionId": 1694783505123
				}
			]
		}]
	}]
}
```

{% endcode %}


# Subscription API

{% hint style="warning" %}
Do not forget to configure subscriptions settings in devtodev to successfully receive data! \
[Android setup](https://docs.devtodev.com/integration/server-api/pages/-MeASFB85TrtEFbUq3Pm#step-3.-settings-on-devtodev-side) \
[iOS setup](https://docs.devtodev.com/integration/server-api/pages/-MeeHqlQ6GNgcFpPS9uN#step-2.-settings-on-devtodev-side)
{% endhint %}

## General provisions

Endpoint:

```http
https://api.devtodev.com/subscriptions/api?apikey={project’s API key}
```

Query type: POST

The query body is a JSON object with fields described in the table below.

### Fields

| Name                                                                                                                                                | Description                                                                                                                                                                                                      |
| --------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`notificationType`**                                                                                                                              | Type of notification                                                                                                                                                                                             |
| **`originalTransactionId`**                                                                                                                         | ID of the original transaction                                                                                                                                                                                   |
| **`transactionId`**                                                                                                                                 | Transaction ID                                                                                                                                                                                                   |
| **`startDateMs`**                                                                                                                                   | Subscription start date in milliseconds in UTC                                                                                                                                                                   |
| **`expiresDateMs`**                                                                                                                                 | Subscription expiration date in milliseconds in UTC                                                                                                                                                              |
| **`product`**                                                                                                                                       | Subscription SKU                                                                                                                                                                                                 |
| **`productType`**                                                                                                                                   | Subscription type (line)                                                                                                                                                                                         |
| **`price`**                                                                                                                                         | Subscription price                                                                                                                                                                                               |
| **`currency`**                                                                                                                                      | Currency of the subscription payment                                                                                                                                                                             |
| **`isTrial`**                                                                                                                                       | The event is flagged if it is trial and is not flagged if it’s a subscription                                                                                                                                    |
| **`gracePeriod`**                                                                                                                                   | Extension of subscription renewal date in days. If this field is filled in and the user did not renew the subscription before the expiration date, then wait for `gracePeriod` days value to flag it as expired. |
| **Fields for user identification (Attention! Regardless of the type of notification, at least one of the user IDs must be specified in the query)** |                                                                                                                                                                                                                  |
| **`idfa`**                                                                                                                                          | Available platforms: iOS                                                                                                                                                                                         |
| **`idfv`**                                                                                                                                          | Available platforms: iOS                                                                                                                                                                                         |
| **`advertisingId`**                                                                                                                                 | Available platforms: Android, Windows                                                                                                                                                                            |
| **`androidId`**                                                                                                                                     | Available platforms: Android                                                                                                                                                                                     |
| **`userId`**                                                                                                                                        | The same as Main ID in user card                                                                                                                                                                                 |
| **`customId`**                                                                                                                                      | <p>String custom user ID</p><p>Available platforms: all platforms</p>                                                                                                                                            |
| **`devtodevId`**                                                                                                                                    | <p>d2d numeric user id. <br>Available platforms: all platforms</p>                                                                                                                                               |

### Notification types

| Notification type  | Description                                                    |
| ------------------ | -------------------------------------------------------------- |
| ***purchase***     | First-time subscription purchase                               |
| ***renewal***      | Active subscription renewal                                    |
| ***refund***       | Money refund                                                   |
| ***cancellation*** | Cancellation of an active subscription and proportional refund |

### Server response

If the notification is accepted, the response status is ***200***. If the query is incorrect, the response status is ***400***. In case of an internal error, the response status is ***500***.

In case the status is ***400*** or ***500***, the error description will be indicated in the query body as a JSON object:

```json
{
  "title": "Bad request",
  "error": "Not set parameter api-key"
}
```

## Notification types and corresponding fields

1. Available notification types for a trial (all postbacks should be flagged as ‘**`isTrial`** = ***true***’):

* ***purchase*** – trial subscription
* ***cancellation*** – subscription cancellation before the end of that trial

2\. Available notification types for subscription purchase:

* ***purchase*** – subscription purchase
* ***renewal*** – subscription renewal
* ***cancellation*** – premature termination of a subscription and proportional refund
* ***refund*** – money refund

### **Trial purchase**&#x20;

| Field name                  | Description                                  |
| --------------------------- | -------------------------------------------- |
| **`notificationType`**      | Notification type = ***purchase***           |
| **`originalTransactionId`** | ID of the original transaction               |
| **`transactionId`**         | Transaction ID                               |
| **`startDateMs`**           | Trial start date in milliseconds in UTC      |
| **`expiresDateMs`**         | Trial expiration date in milliseconds in UTC |
| **`product`**               | Subscription SKU                             |
| **`productType`**           | Subscription type (line)                     |
| **`isTrial`**               | ***true***                                   |

### **Trial cancellation**&#x20;

| Field name                  | Description                                  |
| --------------------------- | -------------------------------------------- |
| **`notificationType`**      | Notification type = ***cancellation***       |
| **`originalTransactionId`** | ID of the original transaction               |
| **`transactionId`**         | Transaction ID                               |
| **`expiresDateMs`**         | Trial expiration date in milliseconds in UTC |
| **`product`**               | Subscription SKU                             |
| **`productType`**           | Subscription type (line)                     |
| **`isTrial`**               | ***true***                                   |

### **Subscription purchase**

| Field name                  | Description                                         |
| --------------------------- | --------------------------------------------------- |
| **`notificationType`**      | Notification type = ***purchase***                  |
| **`originalTransactionId`** | ID of the original transaction                      |
| **`transactionId`**         | Transaction ID                                      |
| **`startDateMs`**           | Subscription start date in milliseconds in UTC      |
| **`expiresDateMs`**         | Subscription expiration date in milliseconds in UTC |
| **`product`**               | Subscription SKU                                    |
| **`productType`**           | Subscription type (line)                            |
| **`isTrial`**               | ***false***                                         |
| **`price`**                 | Subscription price                                  |
| **`currency`**              | Currency of the subscription payment                |
| **`gracePeriod`**           | Extension of subscription renewal date in days      |

### **Subscription renewal**

| Field name                  | Description                                         |
| --------------------------- | --------------------------------------------------- |
| **`notificationType`**      | Notification type = ***renewal***                   |
| **`originalTransactionId`** | ID of the original transaction                      |
| **`transactionId`**         | Transaction ID                                      |
| **`startDateMs`**           | Subscription start date in milliseconds in UTC      |
| **`expiresDateMs`**         | Subscription expiration date in milliseconds in UTC |
| **`product`**               | Subscription SKU                                    |
| **`productType`**           | Subscription type (line)                            |
| **`isTrial`**               | **`false`**                                         |
| **`price`**                 | Subscription price                                  |
| **`currency`**              | Currency of the subscription payment                |
| **`gracePeriod`**           | Extension of subscription renewal date in days      |

### **Subscription cancellation**

| Field name                  | Description                                         |
| --------------------------- | --------------------------------------------------- |
| **`notificationType`**      | Notification type = ***cancellation***              |
| **`originalTransactionId`** | ID of the original transaction                      |
| **`transactionId`**         | Transaction ID                                      |
| **`expiresDateMs`**         | Subscription expiration date in milliseconds in UTC |
| **`product`**               | Subscription SKU                                    |
| **`productType`**           | Subscription type (line)                            |
| **`isTrial`**               | ***false***                                         |

### **Subscription refund**

| Field name                  | Description                                         |
| --------------------------- | --------------------------------------------------- |
| **`notificationType`**      | Notification type = ***refund***                    |
| **`originalTransactionId`** | ID of the original transaction                      |
| **`transactionId`**         | Transaction ID                                      |
| **`expiresDateMs`**         | Subscription expiration date in milliseconds in UTC |
| **`product`**               | Subscription SKU                                    |
| **`productType`**           | Subscription type (line)                            |
| **`isTrial`**               | ***false***                                         |
| **`price`**                 | The amount of money refunded to the user            |
| **`currency`**              | Currency of the subscription payment                |

## **Query example**

```json
POST https://api.devtodev.com/subscriptions/api?apikey=ak-cDNRQl0Lypq4AOUrx8aGGMnmJT1FSebd
{
   "notificationType":"PURCHASE",
   "transactionId":"transactionId",
   "startDateMs":1640072573468,
   "expiresDateMs":1640245373468,
   "product":"com.demo.bundle.weekly",
   "price":90.9,
   "currency":"RUB",
   "isTrial":false,
   "devtodevId":4064192
}

```


# Push API

To use Push API you need to have individual User API token, which can be found in the settings of space.

<figure><img src="/files/Vtemw5MCeZxplBiIf5Do" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
You’ll see the block with User API token on the space settings page only if your tariff plan and access rights allow to use devtodev API. You can reset User API token or create it again on the same page.&#x20;
{% endhint %}

<figure><img src="/files/nbOD0cmEmJKoM2wiA2YY" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
Please note, if you use several spaces, in every space user has an individual User API token.
{% endhint %}

{% hint style="warning" %}
In order to display the notifications on the device, an SDK integration is required.&#x20;
{% endhint %}

{% content-ref url="/pages/-MhDq1hWxd253TB-KPmD" %}
[Push notifications](/integration/integration-of-sdk-v2/push-notifications)
{% endcontent-ref %}

## Request format

The request of assignment should be sent to:

```
​https://www.devtodev.com/api/v1/push/send?user_token=USER_API_TOKEN
```

Where

* `user_token` – individual User API token of a user. It could be sent with both GET and POST methods.
* `v1` – the current version of API.

Request content is sent with POST method in JSON format. &#x20;

The body of a request can contain the following properties:

<table><thead><tr><th width="165">Property</th><th width="140">Type</th><th>Description</th></tr></thead><tbody><tr><td>user_token</td><td>string</td><td><strong>Required.</strong> An individual user API token. It can be found on the space settings page. It is possible to send it with both POST and GET methods.</td></tr><tr><td>app_id</td><td>string</td><td><strong>Required.</strong> An application identifier. It can be found in the application’s settings in the Integration section. Required.</td></tr><tr><td>campaign_tag</td><td>string</td><td><strong>Optional.</strong> The name of a campaign. The grouping of statistics for the assessment of the efficiency of a campaign is made by the name of a campaign. The API Stats report can be found in the Push section.</td></tr><tr><td>pack_id</td><td>string</td><td><strong>Optional.</strong> The unique identifier of a request. It is specified by a developer. It is used for the filtering of repeated sendings of identical requests in case of the loss of connection, etc. Requests with the same identifier can't be repeated during 10 minutes after their sending.</td></tr><tr><td>audience</td><td>array</td><td><p><strong>Required.</strong> An array of mailing audience. The element of an array is an object with an individual for every platform set of user identifiers. It is allowed to specify several known identifiers for one user. The maximum number of elements of an array is 1000.</p><p>The lists of available identifiers can be found in the description to each platform.</p></td></tr><tr><td>ios</td><td>object</td><td>An object containing a notification and its properties for the <a href="/pages/-LyhAMlxDsQsDvsBSH2K">iOS platform.</a></td></tr><tr><td>android</td><td>object</td><td>An object containing a notification and its properties for the <a href="/pages/-LyhAQVlqOCIyMd4kgZA">Android platform</a>.</td></tr><tr><td>win</td><td>object</td><td>An object containing a notification and its properties for the <a href="/pages/-LyhATcSHNEAmDc8dZED">Windows platform</a>. Use this object for any version of Windows (Windows Phone 8.1, Windows Phone 10, Windows 8.1, Windows 10).</td></tr><tr><td>uwp</td><td>object</td><td>An object containing a notification and its properties for the <a href="/pages/-LyhAWwBF2LO7O4aAQr8">Windows</a> 10 / WP 10. Don’t use this object for sending to other versions of Windows.</td></tr></tbody></table>

An example of a request for sending a simple notification to an iOS device:

```
​https://devtodev.com/api/v1/push/send
```

POST

```json
{
    "user_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "app_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "campaign_tag": "campaign name",
    "pack_id": "uniqueid1234",
    "audience": [{
        "idfa": "XXXXX-XXXXX-XXXXXX-XXXXXX"
    },
    {
        "idfa": "YYYYY-YYYYY-YYYYYY-YYYYYY"
    }],
    "ios": {
        "payload": {
            "text": "Hello world!"
        },
        "options": {
            "priority": "normal",
            "expire": 36000
        }
    }
}
```

## Response format

If a request is formed correctly and there are no obstacles for sending a notification to users, the answer is a JSON of the following type:

```json
{
    "status_code": 200,
    "data": {
        "status": "complete",
        "result": {
            "audience": 100500,
            "successful": 100477,
            "erroneous": 23,
            "error_details": []
        }
    }
}
```

where

* `audience` *(number)* – the number of users found.
* `successful` *(number)* – the number of successfully sent notifications.
* `erroneous` *(number)* – the number of notifications rejected by the delivery service.
* `error_details` (array) – a detailed description of reasons of a delivery fail.

In case there is an error in a request, the answer is made in the following format:

```json
{
    "status_code": 400,
    "errors": [{
        "code": 3,
        "msg": "Error description"
    }]
}
```

where

* `status_code` *(number)* – a general status of an error.
* `errors` (array) – an array of error descriptions.
* `code` *(number)* – the exact code of an error from the table of errors.
* `msg` *(string)* – a brief description of an error.

The list of possible errors is given in a table below.

### List of possible errors

<table><thead><tr><th width="100">Status code</th><th width="83">Code</th><th>Value of "msg" field</th><th>Error description</th></tr></thead><tbody><tr><td>500</td><td>1</td><td>Unknown Error</td><td>Unknown error. Please contact devtodev technical support.</td></tr><tr><td>400</td><td>2</td><td>Request body is empty</td><td>The empty body of the request. There is no POST data in the request.</td></tr><tr><td>400</td><td>3</td><td>Malformed json</td><td>JSON error in the body of the request. Fix the formatting error.</td></tr><tr><td>400</td><td>4</td><td>Field not found: %field_name%</td><td>An obligatory field can not be found. You need to complete the request with this field.</td></tr><tr><td>400</td><td>6</td><td>Invalid app id %app id value%</td><td>The requested project can not be found. An unknown application. This error can arise when a user makes a mistake with an app ID or when an application with this ID has been removed.</td></tr><tr><td>401</td><td>11</td><td>Authorization error. Wrong user token %user_token value%</td><td>Authorization error. The set token is wrong. <code>User_token</code> field. User API token.</td></tr><tr><td>401</td><td>12</td><td>Authorization error. User_token is not set.</td><td>Authorization error. <code>User_token</code> field is absent. User API token should be set either as a parameter in GET string of the request or in POST body of the request.</td></tr><tr><td>403</td><td>13</td><td>Access denied. You have no access to the app %app id value%</td><td>Access error. User has no access to this application.</td></tr><tr><td>403</td><td>14</td><td>Access denied. You have no access to the report file %file id value%</td><td>Access error. User has no access to this file. This error can arise if you have to access the application used in a previously created request.</td></tr><tr><td>403</td><td>15</td><td>Access denied. You have no access to API.</td><td>Access error. No access to User API token. This error can arise when access rights have been changed (as a consequence of changing a user role or tariff plan).</td></tr><tr><td>403</td><td>23</td><td>Access denied. You have no access to Push API.</td><td>Access error. This error can arise when you have no rights to Push API for your user role or tariff plan.</td></tr><tr><td>400</td><td>24</td><td>Push service is not enabled in an application settings.</td><td>Push service is disabled. Enable the service on the <a href="/pages/-M1exVsY_QpCEDgJnHzW#push-notifications">settings page</a> of an app.</td></tr><tr><td>400</td><td>25</td><td>The devtodev SDK is not integrated with the application you specified</td><td>The SDK is not integrated.</td></tr><tr><td>400</td><td>26</td><td>No push token has been received from the devtodev SDK integrated with the app you specified. Please make sure the SDK is integrated correctly.</td><td>No push token has been received from the devtodev SDK integrated with the app you specified. Please make sure the SDK is integrated correctly.</td></tr><tr><td>400</td><td>27</td><td>The audience array should not contain more than 1000 elements.</td><td>The audience array contains more than 1000 elements. Reduce the number of users in one request.</td></tr><tr><td>400</td><td>28</td><td>Unexpected value for field %field%. Received value: %value%. Expected values: %values%.</td><td>There is an unexpected value for the field. Use the recommendation and correct the request.</td></tr><tr><td>400</td><td>29</td><td>Unexpected field %field%</td><td>The request contains the field not specified in the documentation. The field must be excluded from the request.</td></tr><tr><td>400</td><td>30</td><td>Invalid value for field %field%. Received value: %value%. Expected: %description%</td><td>Invalid value has been attributed to the field. Use the recommendation and correct the request.</td></tr><tr><td>400</td><td>31</td><td>Notification payload size limit exceeded.</td><td>Reduce the payload size.</td></tr><tr><td>400</td><td>32</td><td>The sending was blocked. The request with the %pack_id% identifier has already been sent during previous 10 minutes.</td><td>Requests with the same identifier can't be repeated during 10 minutes after their sending.</td></tr><tr><td>400</td><td>33</td><td>The requested users have not been found, or there have been no push-token received from them.</td><td>The requested users have not been found, or there have been no push-token received from them.</td></tr></tbody></table>

Besides the errors listed above, `error_details` field may contain errors from the connected notification services. You can find descriptions of such errors in documentation for these services:

* [Android (Firebase)](https://firebase.google.com/docs/cloud-messaging/http-server-ref#error-codes)
* [iOS (APNS)](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns#2947619)
* [Windows (WNS)](https://msdn.microsoft.com/en-us/windows/desktop/hh465435#pncodes_x_wns_notification)


# IOS

## User identifiers

In order to find a user to whom you need to send a notification, it is possible specify one or several available identifiers:

| Property   | Type   | Description                                                                                          |
| ---------- | ------ | ---------------------------------------------------------------------------------------------------- |
| token      | string | Push token. If you use a push token, all fields with other identifiers will be ignored!              |
| idfa       | string | Ad identifier IDFA                                                                                   |
| idfv       | string | Device identifier under vendor IDFV                                                                  |
| userId     | string | User id is applicable if an internal identifier (cross-platform user identifier) is used in your app |
| devtodevId | number | Numeric user identifier in devtodev database.                                                        |

## Notification settings

The object of a message for the iOS platform contains 2 fields:

* payload (object) - describes the main content of a notification
* options (object) -  describes the optional settings of notification delivery

### Notification content settings ("payload" object)

| Property       | Type   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| -------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| title          | string | Optional. A short string describing the purpose of a notification. Apple Watch displays this string as a part of the notification interface. This string is displayed only briefly and should be crafted so that it can be understood quickly. This key was added in iOS 8.2.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| text           | string | The text of an alert message. Required if a notification is not hidden.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| title-loc-key  | string | Optional. The key to a title string in the Localizable.strings file for the current localization. The key string can be formatted with %@ and %n$@ specifiers to take the variables specified in the title-loc-args array. See [Localized Formatted Strings](https://developer.apple.com/library/prerelease/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/TheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH107-SW7) for more information. This key was added in iOS 8.2.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| title-loc-args | string | Optional. Variable string values to appear in place of  format specifiers in title-loc-key. See [Localized Formatted Strings](https://developer.apple.com/library/prerelease/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/TheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH107-SW7) for more information. This key was added in iOS 8.2.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| action-loc-key | string | Optional. If a string is specified, the system displays an alert that includes the “Close” and “View” buttons. The string is used as a key to get a localized string in the current localization to use for the right button’s title instead of “View”. See [Localized Formatted Strings](https://developer.apple.com/library/prerelease/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/TheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH107-SW7) for more information.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| loc-key        | string | Optional. The key to an alert message string in a Localizable.strings file for the current localization (which is set by the user’s language preferences). The key string can be formatted with %@ and %n$@ specifiers to take the variables specified in the loc-args array. See [Localized Formatted Strings](https://developer.apple.com/library/prerelease/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/TheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH107-SW7) for more information.                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| loc-args       | array  | Optional. Variable string values to appear in place of format specifiers in loc-key. See [Localized Formatted Strings](https://developer.apple.com/library/prerelease/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/TheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH107-SW7) for more information.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| launch-image   | string | Optional. The filename of an image file in the app bundle, with or without the filename extension. The image is used as a launch image when users tap the action button or move the action slider. If this property is not specified, the system either uses the previous snapshot, uses the image identified by the UILaunchImageFile key in the app’s Info.plist file, or falls back to Default.png. This property was added in iOS 4.0.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| data           | object | <p>Optional if notification is not hidden.  You can pass custom parameters with messages and use them within an app. For instance, you can activate advertising campaign or any other functionality for the user who has received this message.</p><p>Example:</p><p><code>"data": {</code> <br>        <code>"my\_key": "value",</code> <br>        <code>"my\_another\_key": 15</code> <br><code>}</code></p>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| badge          | number | Optional. The number to display as the badge of the app icon. If this property is absent, the badge is not changed. To remove the badge, set the value of this property to 0.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| sound          | string | Optional. The name of a sound file in the app bundle or in the Library/Sounds folder of the app’s data container. The sound in this file is played as an alert. If the sound file doesn’t exist or "default" is specified as the value, the default alert sound is played. The audio must be in one of the audio data formats that are compatible with system sounds; see [Preparing Custom Alert Sounds](https://developer.apple.com/library/prerelease/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/IPhoneOSClientImp.html#//apple_ref/doc/uid/TP40008194-CH103-SW6) for details.                                                                                                                                                                                                                                                                                                                                                                                              |
| attachment     | object | <p>Optional.<br>Available from devtodev iOS SDK 1.9, Cordova SDK 1.9, Unity SDK 2.3, UE4 SDK 1.9, Air SDK 1.8<br>Added in iOS 10. The possibility to attach images, videos, audio, and GIFs is available in iOS 10. Specify the type of an attachment ("image", "audio" or "video") and URL that leads to a media file. Attention, iOS uses only “https” protocol.</p><p>Example:</p><p><code>"attachment": {</code> <br>        <code>"type": "image",</code> <br>        <code>"url": "<https://domain.com/pic.gif>"</code> <br><code>}</code></p>                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| action         | object | <p>Optional.<br>Available from devtodev iOS SDK 1.9, Cordova SDK 1.9, Unity SDK 2.3, UE4 SDK 1.9, Air SDK 1.8<br>The action after a click on the body of a notification. By default, a click simply opens an app. It is also possible to perform the following actions:</p><ul><li>deeplink (string) - Direct the user to a specific resource, either within your app or on the web.</li><li>url (string) - Open a web page in a mobile browser, or any valid device-level URL such as App Store or app protocol links.</li><li>share (string) - The Share Action drives a user to share your message when they interact with your push notification.</li></ul><p>Examples:</p><p><code>"action": {</code> <br>        <code>"url": "<http://www.domain.com>"</code> <br><code>}</code></p><p><code>"action": {</code> <br>        <code>"deeplink": "your-url-scheme://host/path"</code> <br><code>}</code></p><p><code>"action": {</code> <br>        <code>"share": "Happy holidays!"</code> <br><code>}</code></p> |
| interactive    | object | <p>Optional.<br>Available from devtodev iOS SDK 1.9, Cordova SDK 1.9, Unity SDK 2.3, UE4 SDK 1.9, Air SDK 1.8<br>It is possible to specify one of the existing button templates in an “interactive” object, as well as assign the necessary actions to template buttons. It is possible to assign additional actions only to the button that opens an app. The same set of actions is available: deeplink, url and share. The list of accessible button templates is below in the text.</p><p>Example:</p><p><code>"interactive": {</code> <br>        <code>"template": "dtd\_accept.open\_decline.dismiss",</code> <br>        <code>"buttons": \[{</code> <br>                <code>"id": "accept",</code> <br>                <code>"action": { "url": "<http://www.domain.com/accept>"</code> <br>                <code>}</code> <br>        <code>}]</code> <br><code>}</code></p>                                                                                                                               |

### Notification delivery and display settings (“options” object)

| Property      | Type    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| ------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| sandbox       | boolean | <p>Default value is false ("Production" gateway).<br>Set to true if you need to send notification through the "Sandbox" gateway (for builds signed with a developer certificate).<br></p>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| hidden        | boolean | <p>Switching a notification to the hidden mode. If “true” - a notification will not be displayed to a user, but will be transferred to an app. Such a message must not contain any properties except “data” in the “payload” object.</p><p>Including this key and value means that when your app is launched in the background or resumed, <a href="https://developer.apple.com/reference/uikit/uiapplicationdelegate/1623013-application">application:didReceiveRemoteNotification:fetchCompletionHandler:</a> is called</p>                                                                                                                                                                |
| priority      | string  | <p>Optional. The priority of a notification. Default value is "normal". Specify one of the following values:</p><ul><li>"high" – Send a push message immediately. Notifications with this priority must trigger an alert, sound, or badge on a target device. It is an error to use this priority for a push notification that contains only the content-available key.</li><li>"normal" — Send a push message at a time that takes into account power considerations for a device. Notifications with this priority might be grouped and delivered in bursts. They are throttled, and in some cases are not delivered.</li></ul>                                                            |
| expire        | number  | <p>This option identifies the date when the notification is no longer valid and can be discarded. It is possible to use either relative time in seconds passed since the moment of sending, or to specify the exact date in UNIX epoch format date expressed in seconds (UTC).</p><p>Default value is 7 days (604800 seconds) after sending.</p><p>If this value is nonzero, APNs stores a notification and tries to deliver it at least once repeating the attempt as needed if it is unable to deliver a notification for the first time. If the value is 0, APNs treats the notification as if it expires immediately and does not store the notification or attempt to redeliver it.</p> |
| collapse\_key | string  | Multiple notifications with the same collapse identifier are displayed to a user as a single notification. The value should not exceed 64 bytes.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |

## Button templates

| Name                                     | Description                                                                                                                                   | Template ID                          | Button 1     | Button 2   |         |         |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ------------ | ---------- | ------- | ------- |
|                                          |                                                                                                                                               |                                      | Label        | id         | Label   | id      |
| Yes or No (Open an app)                  | Yes option takes user into an app. No dismisses the notification.                                                                             | dtd\_yes.open\_no.dismiss            | Yes          | yes        | No      | no      |
| Yes or No (Dismiss notification)         | Yes and No options dismiss the notification when clicked without taking a user into an app.                                                   | dtd\_yes.dismiss\_no.dismiss         | Yes          | yes        | No      | no      |
| Accept or Decline (Open the app)         | Accept option takes a user into an app. No dismisses a notification.                                                                          | dtd\_accept.open\_decline.dismiss    | Accept       | accept     | Decline | decline |
| Accept or Decline (Dismiss notification) | Yes and No options dismiss a notification when clicked without taking a user into an app.                                                     | dtd\_accept.dismiss\_decline.dismiss | Accept       | accept     | Decline | decline |
| Shop Now                                 | Shop Now takes user into an app. Should be a different location from a notification action.                                                   | dtd\_shop\_now\.open                 | Shop Now     | shop\_now  |         |         |
| Buy Now                                  | Buy Now takes user into an app. Should be a different location from a notification action.                                                    | dtd\_buy\_now\.open                  | Buy Now      | buy\_now   |         |         |
| Tell me more                             | Deep link to more details about a specific offer or program                                                                                   | dtd\_more\_info.open                 | Tell me more | more\_info |         |         |
| Download                                 | Download deep links users directly to media e.g. wallpaper images, apps, music downloads, file downloads, etc.                                | dtd\_download.open                   | Download     | download   |         |         |
| Share                                    | Pass sharing text through to native OS apps like Facebook and Twitter using the Share action.                                                 | dtd\_share.open                      | Share        | share      |         |         |
| Download or Share                        | Download deep links users directly to media e.g. wallpaper images, apps, music downloads, file downloads, etc. Share the same media socially. | dtd\_download.open\_share.open       | Download     | download   | Share   | share   |
| Shop Now or Share                        | Shop Now takes a user into an app; should be a different location from a notification action.                                                 | dtd\_shop\_now\.open\_share.open     | Shop Now     | shop\_now  | Share   | share   |
| Buy Now or Share                         | Buy Now takes a user into an app; should be a different location from a notification action.                                                  | dtd\_buy\_now\.open\_share.open      | Buy Now      | buy\_now   | Share   | share   |
| Like or Dislike                          | Both options take a user into an app.                                                                                                         | dtd\_like.open\_dislike.open         | Like         | like       | Dislike | dislike |
| Like or Dislike                          | Like option takes user into an app. Dislike dismisses a notification.                                                                         | dtd\_like.open\_dislike.dismiss      | Like         | like       | Dislike | dislike |
| Like                                     | Option takes a user into an app.                                                                                                              | dtd\_like.open                       | Like         | like       |         |         |
| Like and Share                           | Capture user sentiment by allowing users to like a message or share it. Both options take user into an app.                                   | dtd\_like.open\_share.open           | Like         | like       | Share   | share   |
| Add                                      | Add an item: most often a wallet pass or card to your digital Wallet.                                                                         | dtd\_add.open                        | Add          | add        |         |         |
| Save and No                              | Save something for future reference.                                                                                                          | dtd\_save.open                       | Save         | save       |         |         |
| Rate now                                 | Drive users to rate an app in the app store by deep linking from this button.                                                                 | dtd\_rate.open                       | Rate now     | rate       |         |         |
| Search                                   | Deep link to a search functionality within an app                                                                                             | dtd\_search.open                     | Search       | search     |         |         |
| Book now                                 | Deep link to booking flow within an app.                                                                                                      | dtd\_book.open                       | Book now     | book       |         |         |

The text on the buttons, which are on the list of templates, is translated into N languages and displayed to users individually according to the location of a device.

## Examples

### Push

```
​https://devtodev.com/api/v1/push/send
```

POST

```json
{
    "user_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "app_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "campaign_tag": "campaign name",
    "pack_id": "uniqueid1234",
    "audience": [{
        "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "idfa": "XXXXX-XXXXX-XXXXXX-XXXXXX",
        "idfv": "XXXXX-XXXXX-XXXX-XXXXX-XXXX",
        "userId": "xxxxxxxxxxxx"
    }],
    "ios": {
        "payload": {
            "title": "Title of the notification",
            "text": "Notification content.",
            "data": {
                "key1": "value",
                "key2": "15"
            },
            "badge": 11,
            "sound": "bingbong.aiff",
            "attachment": {
                "type": "image",
                "url": "https://domain.com/pic.png"
            },
            "action": {
                "url": "http://www.domain.com"
            },
            "interactive": {
                "template": "dtd_accept.open_decline.dismiss",
                "buttons": [{
                    "id": "accept",
                    "action": {
                        "url": "http://www.domain.com/accept"
                    }
                }]
            }
        },
        "options": {
            "hidden": false,
            "priority": "normal",
            "expire": "36000",
            "collapse_key": "qwerqwer"
        }
    }
}
```

### Hidden push

```
​https://devtodev.com/api/v1/push/send
```

POST

```json
{
    "user_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "app_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "campaign_tag": "campaign name",
    "pack_id": "uniqueid1234",
    "audience": [{
        "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "idfa": "XXXXX-XXXXX-XXXXXX-XXXXXX",
        "idfv": "XXXXX-XXXXX-XXXX-XXXXX-XXXX",
        "userId": "xxxxxxxxxxxx"
    }],
    "ios": {
        "payload": {
            "data": {
                "key1": "value",
                "key2": "15"
            },
            "badge": "11"
        },
        "options": {
            "hidden": true,
            "priority": "normal",
            "expire": "36000"
        }
    }
}
```


# Android

## User identifiers

In order to find a user to whom you need to send a notification, it is possible to specify one or several available identifiers:

| Property      | Type   | Description                                                                                           |
| ------------- | ------ | ----------------------------------------------------------------------------------------------------- |
| token         | string | Push token. If you use push token, all fields with other identifiers will be ignored!                 |
| advertisingId | string | Advertising ID                                                                                        |
| androidId     | string | Android ID                                                                                            |
| serialId      | string | Serial ID                                                                                             |
| userId        | string | User id is applicable if an internal identifier (cross-platform user identifier) is used in your app. |
| devtodevId    | number | Numeric user identifier in devtodev database.                                                         |

## Notification settings

The object of a message for the Android platform contains 2 fields:

* payload (object) - describes the main content of a notification
* options (object) - describes the optional settings of notification delivery

### Notification content settings ("payload” object)

| Property    | Type    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ----------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| title       | string  | A short string describing the purpose of a notification.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| text        | string  | The text of an alert message.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| data        | object  | <p>Optional if notification is not hidden.  You can pass custom parameters with messages and use them within an app. For instance, you can activate advertising campaign or any other functionality for a user who has received this message.</p><p>Example:</p><p><code>"data": {</code> <br>    <code>"my\_key": "value",</code> <br>    <code>"my\_another\_key": "15"</code> <br><code>}</code></p>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| small\_icon | string  | Optional. You may use an icon from resources of your app. You should use resource name of the icon in this field.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| color       | string  | <p>Optional. The parameter recolors the small icon of an app displayed in a notification in the specified color ("#RRGGBB"). This N parameter on Android also recolors the name of an app and texts on notification buttons.</p><p>Example:</p><p><code>"color": "#ff0000"</code></p>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| icon        | string  | Optional. Use the URL to a notification icon or a resource name.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| image       | string  | Optional. The value is image URL. Images should be ≤ 450dp wide, \~2:1 aspect. The image will be center cropped.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| sound       | string  | Optional. A media file to play in place of a default sound. If a sound file doesn’t exist or default is specified as the value, the default notification sound is played. The audio must be in one of the audio data formats that are compatible with system sounds. Supports “default” or the filename of a sound resource bundled in an app. Android sound files must reside in /res/raw/ If the option is not used, a notification comes silently.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| badge       | int     | <p>Optional. This property was added in Android O. The value of the badge on the home screen app icon.<br>If not specified, the badge is not changed. If set to 0, the badge is removed.</p>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| vibration   | boolean | Optional. In case of "true" during notification delivery a device will vibrate (if permission VIBRATION is received)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| led\_color  | string  | <p>Optional. LED hex color  ("#RRGGBB"), a device will do its best approximation.</p><p>Example:</p><p><code>"led\_color": "#ff0000"</code></p>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| action      | object  | <p>Optional. The action after a click on the body of a notification. By default, a click opens an app. It is also possible to perform the following actions:</p><ul><li>deeplink (string)  - Direct a user to a specific resource either within your app or on the web.</li><li>url (string) - Open a web page in a mobile browser, or any valid device-level URL such as Google Play or app protocol links.</li><li>share (string) - The Share Action drives a user to share your message when they interact with your push notification.</li></ul><p>Examples:</p><p><code>"action": {</code> <br>    <code>"url": "<http://www.domain.com>"</code> <br><code>}</code><br><br><code>"action": {</code> <br>    <code>"deeplink": "your-url-scheme://host/path"</code> <br><code>}</code><br><br><code>"action": {</code> <br>    <code>"share": "Happy holidays!"</code> <br><code>}</code></p>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| interactive | object  | <p>Optional. It is possible to specify an array of notification buttons in an “interactive” object. Each button should contain the following properties:</p><ul><li>id (string) - The button identifier. It is transferred to an app.</li><li>text (string) - A text on a button</li><li><p>handling (string) - The type of processing</p><ul><li>background - The button closes a notification. The parameters of a notification are transferred to an app, but an app does not open. It is impossible to tie an action to a button in this regime.</li><li>foreground  - The button opens an app. The parameters of a notification are transferred to an app, but an app does not open. It is possible to tie an action to a button in this regime. The list of actions and the principle is analogical to actions with the body of a message.</li></ul></li></ul><p>Example:</p><p><code>"interactive": {</code> <br>    <code>"buttons": \[{</code> <br>        <code>"id": "accept",</code> <br>        <code>"text": "Accept",</code> <br>        <code>"handling":"foreground",</code> <br>        <code>"action": {</code> <br>                <code>"url": "<http://www.domain.com/accept>"</code> <br>        <code>},</code> <br>        <code>{</code> <br>        <code>"id": "decline",</code> <br>        <code>"text": "Decline",</code> <br>        <code>"handling":"background"</code> <br>    <code>}]</code> <br><code>}</code></p> |

### Notification delivery and display settings ("options" object)

| Property          | Type    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| ----------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| hidden            | boolean | Switching the notification to the hidden mode. If “true” - a notification will not be displayed to a user, but will be transferred to an app. Such a message must not contain any properties except data in the “payload” object.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| priority          | string  | <p>Optional. The priority of a notification. Default value is "normal". Specify one of the following values:</p><ul><li>high" – GCM attempts to deliver high priority messages immediately allowing the GCM service to wake a sleeping device when possible and open a network connection to your app server. Apps with instant messaging, chat, or voice call alerts, for example, generally need to open a network connection and make sure GCM delivers the message to the device without delay. Set high priority only if the message is time-critical and requires the user’s immediate interaction, and beware that setting your messages to high priority contributes to a battery drain more compared to normal priority messages.</li><li>"normal" — This is the default priority for message delivery. Normal priority messages won't open network connections on a sleeping device, and their delivery may be delayed to preserve a battery. For less time-sensitive messages (such as notifications of new email or other data to sync) choose normal delivery priority.</li></ul> |
| expire            | number  | <p>This option identifies the date when a notification is no longer valid and can be discarded. It is possible to use either relative time in seconds passed since the moment of sending, or specify an exact date in UNIX epoch format expressed in seconds (UTC).</p><p>Default value is 7 days (604800 seconds) after sending. Max. relative value for Android platform is 2 419 200 seconds (4 weeks).</p><p>Keep in mind that an "expire" value of 0 means messages that can't be delivered immediately will be discarded. However, as long as such messages are never stored, this provides the best latency for sending notifications.</p>                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| collapse\_key     | string  | <p>Optional. Notifications are not collapsible by default.</p><p>If multiple messages are sent with this key, the most recent message will suppress all previous unread messages with the same key.</p><p>Collapsible messages are a better choice from a performance standpoint provided your application doesn't need to use non-collapsible messages. However, if you use collapsible messages, remember that GCM only allows a maximum of 4 different collapse keys to be used by the GCM connection server per registration token at any given time. You must not exceed this number, or it could cause unpredictable consequences.</p>                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| channel\_id       | string  | Optional. The [notification's channel id](https://developer.android.com/preview/features/notification-channels.html) (this property was added in Android O). The app must create a channel with this ID before any notification with this key is received. If you don't send this key in the request, or if the channel id provided has not yet been created by your app, devtodev SDK uses the channel id specified by default ("channel\_id": "\_devtodev" - name: General notifications).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| badge\_icon\_type | int     | <p>Optional. This property was added in Android O (API level 26).<br>0 - Default. If this notification is being shown as a badge, always show as a number.<br>1 - If this notification is being shown as a badge, use the getSmallIcon() to represent this notification.<br>2 - If this notification is being shown as a badge, use the getLargeIcon() to represent this notification.</p>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |

## Examples

### Visible push notification

```
​https://devtodev.com/api/v1/push/send
```

POST

```json
{
    "user_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "app_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "campaign_tag": "campaign name",
    "pack_id": "uniqueid1234",
    "audience": [{
        "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "advertisingId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "androidId": "xxxxxxxxxxxxxxxxxxxxxxxxx",
        "userId": "xxxxxxxxxxxx"
    }],
    "android": {
        "payload": {
            "title": "Title of the notification",
            "text": "Notification content.",
            "data": {
                "key1": "value",
                "key2": "15"
            },
            "small_icon": "smallicon",
            "icon": "midicon",
            "image": "https://domain.com/pic.png",
            "sound": "bingbong",
            "vibration": true,
            "led_color": "#ff0000",
            "color": "#ff0000",
            "action": {
                "url": "http://www.domain.com"
            },
            "interactive": {
                "buttons": [{
                    "id": "accept",
                    "text": "Accept",
                    "handling": "foreground",
                    "action": {
                        "url": "http://www.domain.com/accept"
                    }
                }, {
                    "id": "decline",
                    "text": "Decline",
                    "handling": "background"
                }]
            }
        },
        "options": {
            "hidden": false,
            "priority": "normal",
            "expire": 36000,
            "collapse_key": "collapse1"
        }
    }
}
```

### Hidden push notification

```
​https://devtodev.com/api/v1/push/send
```

POST

```json
{
    "user_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "app_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "campaign_tag": "campaign name",
    "pack_id": "uniqueid1234",
    "audience": [{
        "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "advertisingId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "androidId": "xxxxxxxxxxxxxxxxxxxxxxxxx",
        "userId": "xxxxxxxxxxxx"
    }],
    "android": {
        "payload": {
            "data": {
                "key1": "value",
                "key2": "15"
            }
        },
        "options": {
            "hidden": true,
            "priority": "normal",
            "expire": 36000
        }
    }
}
```


# Windows UWP

devtodev Push API supports two different formats of notification sending to Windows operational systems:

* **UWP format described in this section can be used only for Windows 10 and Windows Phone 10.** It is described by the "uwp" object.
* [The universal format ](/integration/server-api/push-api/windows)works on a basis of Legacy tiles and toast schema. It can be used for Windows Phone 8.1, Windows Phone 10, Windows 8.1, Windows 10. It is described by the "windows" object.

**Use this object (“uwp”) if your clients are users of OS Windows 10 and Windows Phone 10 only.** With the help of this method it is possible to realize more opportunities offered by Windows 10.

## User identifiers

In order to find an addressee to whom you need to send a notification, it is possible to specify one or several available identifiers:

| Property      | Type   | Description                                                                                           |
| ------------- | ------ | ----------------------------------------------------------------------------------------------------- |
| token         | string | Push token. If you use push token, all fields with other identifiers will be ignored!                 |
| advertisingId | string | Advertising ID                                                                                        |
| serialId      | string | Hardware serial number                                                                                |
| userId        | string | User id is applicable if an internal identifier (cross-platform user identifier) is used in your app. |
| devtodevId    | number | Numeric user identifier in devtodev database.                                                         |

## Notification settings

Object "uwp" can contain 2 properties:

1. The property that describes a message can be represented by one of 4 possible objects:\
   &#x20;\- toast (object) - the result of sending is the delivery of toast-notification\
   &#x20;\- raw (object) - sends a hidden toast-notification\
   &#x20;\- tile (object) - changes the  tile content of an app\
   &#x20;\- badge (object) - changes the value of a badge field displayed on a tile
2. The property that describes additional parameters of a message is represented by the object “options”. Optional.

### Toast-notification content ("toast" object properties):

| Property                                      | Type   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| --------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| scenario                                      | string | Optional. Available values: "default", "alarm", "reminder", "incomingCall". Default value is "default". You do not need this unless your scenario is to pop an alarm, reminder, or incoming call. Do not use this just for keeping your notification persistent on screen.                                                                                                                                                                                                                                     |
| title                                         | string | Required. A short string describing the purpose of a notification.                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| text                                          | string | Required. Main text of a notification.                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| text2                                         | string | Optional. Additional text of a notification.                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| data                                          | object | <p>Optional if notification is not hidden.  You can pass custom parameters with messages and use them within an app. For instance, you can activate advertising campaign or any other functionality for user who has received this message.</p><p>Example:</p><p><code>"data": {</code>                                                                                                                                                                                                                        |
| <br>    <code>"my\_key": "value",</code>      |        |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| <br>    <code>"my\_another\_key": "15"</code> |        |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| <br><code>}</code></p>                        |        |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| icon                                          | string | <p>Optional. To replace the application icon (that shows up on the top left corner of the toast) use the URL or the resource name.</p><p>In Windows 10 the image is expressed using the URI of the image source, using one of these protocol handlers:</p><ul><li>A web-based image: http\:// or https\://</li><li>An image included in the app package: ms-appx:///</li><li>An image saved to local storage: ms-appdata:///local/</li><li>A local image (Only supported for desktop apps.): file://</li></ul> |
| image                                         | string | Optional. Image inside the toast body, below the text. Use the URL or the resource name.                                                                                                                                                                                                                                                                                                                                                                                                                       |
| sound                                         | string | <p>Optional. The media file to play in place of the default sound. If the option is not used, the notification comes silently. This property can have one of the following string values:</p><ul><li>Default                                                                                                                                                                                                                                                                                                   |
| IM                                            |        |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |

</li><li>Mail</li><li>Reminder</li><li>SMS</li><li>Looping.Alarm</li><li>Looping.Alarm2</li><li>Looping.Alarm3</li><li>Looping.Alarm4</li><li>Looping.Alarm5</li><li>Looping.Alarm6</li><li>Looping.Alarm7</li><li>Looping.Alarm8</li><li>Looping.Alarm9</li><li>Looping.Alarm10</li><li>Looping.Call</li><li>Looping.Call2</li><li>Looping.Call3</li><li>Looping.Call4</li><li>Looping.Call5</li><li>Looping.Call6</li><li>Looping.Call7</li><li>Looping.Call8</li><li>Looping.Call9</li><li>Looping.Call10</li></ul><p>On mobile platform this property can also contain the path to a local audio file with one of the following prefixes:</p><ul><li>ms-appx:///</li><li>ms-appdata:///</li></ul>                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| action      | object | <p>Optional. Action after a click on the body of a notification. By default, a click simply opens an app. It is also possible to perform the following actions:</p><ul><li>deeplink - Direct the user to a specific resource, either within your app or on the web.</li><li>url - Open a web page in a mobile browser, or any valid device-level URL such as Windows Store or app protocol links.</li><li>share - The Share Action drives a user to share your message when they interact with your push notification.</li></ul><p>Examples:<br></p><p><code>"action": {</code> <br>    <code>"url": "http://www.domain.com"</code> <br><code>}</code><br><br><code>"action": {</code> <br>    <code>"deeplink": "your-url-scheme://host/path"</code> <br><code>}</code></p><p><br><code>"action": {</code> <br>    <code>"share": "Happy holidays!"</code> <br><code>}</code></p>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| interactive | object | <p>Optional. It is possible to specify an array of notification buttons in the “interactive” object. Each button must contain the following properties:</p><ul><li>id (string) - Button identifier. It is transferred to an app</li><li>text (string) - Text on a button</li><li><p>handling (string) - The type of processing</p><ul><li>background - A button closes an app. Notification parameters are transferred to an app, but an app doesn’t open. It is impossible to tie an action to a button in this regime.</li><li>foreground  - A button opens an app. Notification parameters are transferred to an app, but an app doesn’t open. It is possible to tie an action to a button in this regime. The list of actions and the principle is analogical to actions with the body of a message.</li></ul></li></ul><p>Example:</p><p><code>"interactive": {</code> <br>    <code>"buttons": [{</code> <br>        <code>"id": "accept",</code> <br>        <code>"text": "Accept",</code> <br>        <code>"handling":"foreground",</code> <br>        <code>"action": {</code> <br>            <code>"url": "http://www.domain.com/accept"</code> <br>        <code>},</code> <br>        <code>{</code> <br>        <code>"id": "decline",</code> <br>        <code>"text": "Decline",</code> <br>        <code>"handling":"background",</code> <br>    <code>}]</code> <br><code>}</code></p> |

Example

```json
{
    "user_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "app_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "campaign_tag": "campaign name",
    "pack_id": "uniqueid1234",
    "audience": [{
        "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "advertisingId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "serialId": "xxxxxxxxx",
        "customUid": "xxxxxxxxxxxx"
    }],
    "uwp": {
        "toast": {
            "scenario": "default",
            "title": "Notification title",
            "text": "First notification string",
            "text2": "Second notification string",
            "data": {
                "key1": "value",
                "key2": "value"
            },
            "icon": "https://domain.com/pic.png",
            "image": ["https://domain.com/pic.png", ""],
            "sound": "Default",
            "action": {
                "url": "http://www.domain.com"
            },
            "interactive": {
                "buttons": [{
                    "id": "accept",
                    "text": "Accept",
                    "handling": "foreground",
                    "action": {
                        "url": "http://www.domain.com/accept"
                    }
                }, {
                    "id": "decline",
                    "text": "Decline",
                    "handling": "background"
                }]
            }
        },
        "options": {
            "expire": 3600
        }
    }
}
```

### Tile ("tile" object properties)

In UWP format every of tile’s size must be described separately, therefore, the object “tile” can contain up to 4 properties:

* small
* medium
* wide
* large   (only for desktop)

Each of these sizes is described by an object with the following properties:

| Property                                                                                                                                                                                                                                                   | Type  | Description                                                                                                                                                                                                                                                                                    |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| content                                                                                                                                                                                                                                                    | array | <p>As long as tile’s content is dynamic, it is described by an array, the elements of which can be objects describing either text strings or illustrations.<br></p><p><strong>Text element object properties:</strong></p><ul><li>text (string) - Required. The displayed string.</li><li>wrap |
| (boolean) - Optional. By default, text doesn't wrap and will continue off the edge of the tile. Set true to set text wrapping on a text element.</li><li><p>style                                                                                          |       |                                                                                                                                                                                                                                                                                                |
| (string                                                                                                                                                                                                                                                    |       |                                                                                                                                                                                                                                                                                                |
| ) - Optional. Styles control the font size, color, and weight of text elements. There is a number of available styles including a "subtle" variation of each style that sets the opacity to 60%, which usually makes the text color a shade of light gray. |       |                                                                                                                                                                                                                                                                                                |
| <br>Basic text styles:                                                                                                                                                                                                                                     |       |                                                                                                                                                                                                                                                                                                |

</p><ul><li>caption (12 effective pixels height, regular weight)</li><li>body (15 epx, regular)</li><li>base (15 epx,    semibold)</li><li>subtitle (20 epx, regular)</li><li>title (24 epx, semilight)</li><li>subheader (34 epx, light)</li><li>header (46 epx, light)</li></ul><p></p><p>Numeral text style variations: (These variations reduce the line height so that content above and below come much closer to the text.)</p><ul><li>titleNumeral</li><li>subheaderNumeral</li><li>headerNumeral</li></ul><p></p><p>Subtle text style variations: (Each style has a subtle variation that gives the text a 60% opacity, which usually makes the text color a shade of light gray.)</p><ul><li>captionSubtle</li><li>bodySubtle</li><li>baseSubtle</li><li>subtitleSubtle</li><li>titleSubtle</li><li>titleNumeralSubtle</li><li>subheaderSubtle</li><li>subheaderNumeralSubtle</li><li>headerSubtle</li><li>headerNumeralSubtle</li></ul></li></ul><p><strong>Image element object properties</strong></p><ul><li>image (string) - Required. Image URI</li><li>remove_margin (boolean) - Optional. By default, inline images have an 8-pixel margin between any content above or below the image. Set true to  remove margin.</li><li>align (string) - Optional. Images can be set to align "left", "center", or "right". This will also cause images to display at their native resolution instead of stretching to fill width.</li><li>crop (string) - Optional. Default value is "none". Images can be cropped into a circle in case of "circle" value.</li><li>placement (string) - Optional. You can specify an image that "peeks” in from the top of a tile or set a "background" image.</li></ul><p><strong>Group element object</strong> <strong>properties</strong></p><ul><li>group (array) - Required. Array of subgroup elements.</li></ul><p><strong>Subgroup element object (used inside groups only)</strong></p><ul><li>subgroup (array) - Required. Array of text or/and image elements.</li></ul> |
| branding | object | <p>You can control the branding on the bottom of a live tile (the display name and corner logo) by using this property. <br><strong>Branding object properties:</strong></p><ul><li>type (string) - You can choose to display "none", only the "name", only the "logo", or both with "nameAndLogo". <em>Windows Mobile doesn't support a corner logo, so "logo" and "nameAndLogo" default to"name" on mobile.</em></li><li>display_name (string) - Optional. You can override the display name of a notification by entering the text string of your choice.</li></ul>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| v\_align | string | You can control the vertical alignment of content on your tile by using this property. By default, everything is vertically aligned to the "top", but you can also align content to the "bottom" or "center".                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| overlay  | number | <p>Optional. You can set a black overlay on your background image using this property, which accepts integers from 0-100 with 0 being no overlay and 100 being full black overlay.</p><p>If you don't specify an overlay, the background image opacity defaults to 20% and the peek image opacity defaults to 0%.</p>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |

**Example**

```json
{
    "user_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "app_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "campaign_tag": "campaign name",
    "pack_id": "uniqueid1234",
    "audience": [{
        "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "advertisingId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "serialId": "xxxxxxxxx",
        "customUid": "xxxxxxxxxxxx"
    }],
    "uwp": {
        "tile": {
            "medium": {
                "content": [{
                    "text": "First sting",
                    "wrap": "true",
                    "style": "base",
                    "align": "center"
                }, {
                    "image": "https://domain.com/pic.png",
                    "remove_margin": "true",
                    "align": "center",
                    "crop": "circle",
                    "placement": "background"
                }],
                "v_align": "center",
                "overlay": "60",
                "branding": {
                    "type": "none"
                }
            },
            "wide": {
                "content": [{
                    "text": "First string",
                    "wrap": "true",
                    "style": "base",
                    "h_align": "center"
                }, {
                    "image": "Assets\\Apps\\Weather\\MostlyCloudy.png",
                    "remove_margin": "true",
                    "align": "center",
                    "crop": "circle",
                    "placement": "background"
                }],
                "v_align": "center",
                "branding": {
                    "type": "nameAndLogo",
                    "display_name": "brandname"
                }
            }
        },
        "options": {
            "expire": 3600
        }
    }
}
```

### Hidden notification ("raw" object properties):

| Property | Type   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| -------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| data     | object | <p>Required. You can pass custom parameters with messages and use them within an app. For instance, you can activate advertising campaign or any other functionality for user who has received this message.</p><p>Example:</p><p><code>"data": {</code> <br>    <code>"my\_key": "value",</code> <br>    <code>"my\_another\_key": "15"</code> <br><code>}</code></p>                                                                                                                                                                                                                                                                                                                        |
| badge    | string | <p>Optional. A number from 1 to 99. A value of 0 is equivalent to the glyph value "none" and will clear a badge.</p><p>Instead of a number, a badge can display one of a non-extensible set of status glyphs:</p><ul><li>activity</li><li>none</li><li>alarm</li><li>alert</li><li>attention</li><li>available</li><li>away</li><li>busy</li><li>error</li><li>newMessage</li><li>paused</li><li>playing</li><li>unavailable</li></ul><p>It is also possible to send values incrementing or decrementing the current value in a “+2”, “-1” format. In case the previous value was the glyph or 0, the value will be increment. Decrement does not influence the zero value and the glyph.</p> |

Example

```json
{
    "user_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "app_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "campaign_tag": "campaign name",
    "pack_id": "uniqueid1234",
    "audience": [{
        "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "advertisingId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "serialId": "xxxxxxxxx",
        "customUid": "xxxxxxxxxxxx"
    }],
    "uwp": {
        "raw": {
            "data": {
                "key1": "value",
                "key2": "value"
            },
            "badge": "+1"
        }
    }
}
```

### Badge ("badge" object properties):

| Property | Type   | Description                                                                                                                                                                                                                                                                                                                                                                                                                 |
| -------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| badge    | string | <p>A number from 1 to 99. A value of 0 is equivalent to the glyph value "none" and will clear a badge.</p><p>Instead of a number a badge can display one of a non-extensible set of status glyphs:</p><ul><li>activity</li><li>none</li><li>alarm</li><li>alert</li><li>attention</li><li>available</li><li>away</li><li>busy</li><li>error</li><li>newMessage</li><li>paused</li><li>playing</li><li>unavailable</li></ul> |

**Example**

```json
{
    "user_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "app_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "campaign_tag": "campaign name",
    "pack_id": "uniqueid1234",
    "audience": [{
        "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "advertisingId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "serialId": "xxxxxxxxx",
        "customUid": "xxxxxxxxxxxx"
    }],
    "uwp": {
        "badge": {
            "badge": "99"
        }
    }
}
```


# Windows

devtodev Push API supports two different formats of notification sending to Windows operational systems:

* Universal format described in this section works on the basis of Legacy tiles and toast schema. It can be used on Windows Phone 8.1, Windows Phone 10, Windows 8.1, Windows 10. It is described by the "win" object.
* [UWP ](/integration/server-api/push-api/windows-uwp)format - can be used only for Windows 10 and Windows Phone 10. It is described by the "uwp" object.

## User identifiers

In order to find a user to whom you need to send a notification, you can specify one or several available identifiers:

| Property      | Type   | Description                                                                                           |
| ------------- | ------ | ----------------------------------------------------------------------------------------------------- |
| token         | string | Push token. If you use push token, all fields with other identifiers will be ignored!                 |
| advertisingId | string | Advertising ID                                                                                        |
| serialId      | string | Hardware serial number                                                                                |
| userId        | string | User id is applicable if an internal identifier (cross-platform user identifier) is used in your app. |
| devtodevId    | number | Numeric user identifier in devtodev database.                                                         |

## Notification settings

Object "windows" can contain two properties :

1. The property that describes a message can be represented by one of 4 possible objects:

* toast (object) - the result of sending is the delivery of toast-notification
* raw (object) - sends a hidden toast-notification
* tile (object) - changes tile content of an app
* badge (object) - changes the value of a badge field displayed in a tile of an app

1. The property that describes additional parameters of a message represented by the object “options”. Optional.

## Toast-notification content ("toast" object properties):

| Property    | Type   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| ----------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| title       | string | Required. A short string describing the purpose of a notification.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| text        | string | Required. Main text of a notification message .                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| text2       | string | Optional. Additional text of a notification.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| data        | object | <p>Optional. You can pass custom parameters with messages and use them within an app. For instance, you can activate advertising campaign or any other functionality for the user who has received this message.</p><p>Example:</p><p><code>"data": {</code> <br>    <code>"my\_key": "value",</code> <br>    <code>"my\_another\_key": "15"</code> <br><code>}</code></p>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| icon        | string | <p>Optional. To replace the application icon (that shows up on the top left corner of the toast) use the URL or the resource name.</p><p>Any toast shown on Windows Phone 8.1 does not display the images (the app icon only). In Windows 10 the image is expressed using the URI of the image source, using one of these protocol handlers:</p><ul><li>A web-based image: http\:// or https\://</li><li>An image included in the app package: ms-appx:///</li><li>An image saved to local storage: ms-appdata:///local/</li><li>A local image (Only supported for desktop apps.): file:///</li></ul>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| sound       | string | <p>Optional. A media file to play in place of a default sound. If the option is not used, a notification comes silently. This property can have one of the following string values:</p><ul><li>Default</li><li>IM</li><li>Mail</li><li>Reminder</li><li>SMS</li><li>Looping.Alarm</li><li>Looping.Alarm2</li><li>Looping.Alarm3</li><li>Looping.Alarm4</li><li>Looping.Alarm5</li><li>Looping.Alarm6</li><li>Looping.Alarm7</li><li>Looping.Alarm8</li><li>Looping.Alarm9</li><li>Looping.Alarm10</li><li>Looping.Call</li><li>Looping.Call2</li><li>Looping.Call3</li><li>Looping.Call4</li><li>Looping.Call5</li><li>Looping.Call6</li><li>Looping.Call7</li><li>Looping.Call8</li><li>Looping.Call9</li><li>Looping.Call10</li></ul><p>On mobile platform, this property can also contain the path to a local audio file, with one of the following prefixes:</p><ul><li>ms-appx:///</li><li>ms-appdata:///</li></ul>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| action      | object | <p>Optional.  Available for Windows 10 and WP 10 only. Action after a click on the body of a notification. By default, a click simply opens an app. It is also possible to perform the following actions:</p><ul><li>deeplink (string)  - Direct the user to a specific resource, either within your app or on the web.</li><li>url (string)  - Open a web page in a mobile browser, or any valid device-level URL such as Windows Store or app protocol links.</li><li>share (string) - The Share Action drives a user to share your message when they interact with your push notification.</li></ul><p>Examples:</p><p><code>"action": {</code> <br>    <code>"url": "<http://www.domain.com>"</code> <br><code>}</code><br><br><code>"action": {</code> <br>    <code>"deeplink": "your-url-scheme://host/path"</code> <br><code>}</code></p><p><br><code>"action": {</code> <br>    <code>"share": "Happy holidays!"</code> <br><code>}</code></p>                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| interactive | object | <p>Optional. Available for Windows 10 and WP 10 only. It is possible to specify an array of notification buttons in the “interactive” object. Each button must contain the following properties:</p><ul><li>id (string) - Button identifier. It is transferred to an app</li><li>text (string) - Text on a button</li><li><p>handling (string) - The type of processing</p><ul><li>background - A button closes an app. Notification parameters are transferred to an app, but an app doesn’t open. It is impossible to tie an action to a button in this regime.</li><li>foreground  - A button opens an app. Notification parameters are transferred to an app. It is possible to tie an action to a button in this regime. The list of actions and the principle is analogical to actions with the body of a message.</li></ul></li></ul><p>Example:</p><p><code>"interactive": {</code> <br>    <code>"buttons": \[{</code> <br>        <code>"id": "accept",</code> <br>        <code>"text": "Accept",</code> <br>        <code>"handling":"foreground",</code> <br>        <code>"action": {</code> <br>            <code>"url": "<http://www.domain.com/accept>"</code> <br>        <code>},</code> <br>        <code>{</code> <br>        <code>"id": "decline",</code> <br>        <code>"text": "Decline",</code> <br>        <code>"handling":"background",</code> <br>    <code>}]</code> <br><code>}</code></p> |

**Example**

```json
{
    "user_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "app_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "campaign_tag": "campaign name",
    "pack_id": "uniqueid1234",
    "audience": [{
        "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "advertisingId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "serialId": "xxxxxxxxx",
        "customUid": "xxxxxxxxxxxx"
    }],
    "win": {
        "toast": {
            "title": "Toast title",
            "text": "Toast message",
            "data": {
                "key1": "value",
                "key2": "15"
            },
            "icon": "https://domain.com/pic.png",
            "sound": "default",
            "action": {
                "url": "http://www.domain.com"
            },
            "interactive": {
                "buttons": [{
                    "id": "accept",
                    "text": "Accept",
                    "handling": "foreground",
                    "action": {
                        "url": "http://www.domain.com/accept"
                    }
                }, {
                    "id": "decline",
                    "text": "Decline",
                    "handling": "background"
                }]
            }

        },
        "options": {
            "expire": 36000
        }
    }
}
```

## Hidden notification ("raw" object):

| Property | Type   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| -------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| data     | object | <p>Required. You can pass custom parameters with messages and use them within an app. For instance, you can activate advertising campaign or any other functionality for user who has received this message.</p><p>Example:</p><p><code>"data": {</code> <br>    <code>"my\_key": "value",</code> <br>    <code>"my\_another\_key": "15"</code> <br><code>}</code></p>                                                                                                                                                                                                                                                                                                                        |
| badge    | string | <p>Optional. A number from 1 to 99. A value of 0 is equivalent to the glyph value "none" and will clear a badge.</p><p>Instead of a number, a badge can display one of a non-extensible set of status glyphs:</p><ul><li>activity</li><li>none</li><li>alarm</li><li>alert</li><li>attention</li><li>available</li><li>away</li><li>busy</li><li>error</li><li>newMessage</li><li>paused</li><li>playing</li><li>unavailable</li></ul><p>It is also possible to send values incrementing or decrementing the current value in a “+2”, “-1” format. In case the previous value was the glyph or 0, the value will be increment. Decrement does not influence the zero value and the glyph.</p> |

**Example**

```json
{
    "user_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "app_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "campaign_tag": "campaign name",
    "pack_id": "uniqueid1234",
    "audience": [{
        "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "advertisingId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "serialId": "xxxxxxxxx",
        "customUid": "xxxxxxxxxxxx"
    }],
    "win": {
        "raw": {
            "data": {
                "key1": "value",
                "key2": "15"
            },
            "badge": "+1"
        }
    }
}
```

## Tile-notification content ("tile" object):

| Property     | Type    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| ------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| text1        | string  | First string on the tile.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| with\_header | boolean | Set True to mark "text1" field as a heading to show it larger.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| text2        | string  | Second string on the tile.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| text3        | string  | Third string on the tile.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| text4        | string  | Fourth string on the tile.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| image        | string  | <p>Optional.Application-relative or Internet URI of the image.</p><p>Example:</p><ul><li>"red.jpg" (if image is stored in an app’s installation directory or local storage folder)</li><li>"<http://my.domain.com/img/red.jpg>" (for Internet URIs).</li></ul><p>We recommend using images with size 336 pixels by 336 pixels.</p><p>Important Note:</p><p>If you need to use remote Internet URIs for Tile images, you must take the following steps:</p><ul><li>Ensure the remote image file size is less than 150 KB.</li><li>Ensure the image can be downloaded in 60 seconds or less.</li></ul> |

**Example**

```json
{
    "user_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "app_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "campaign_tag": "campaign name",
    "pack_id": "uniqueid1234",
    "audience": [{
        "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "advertisingId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "serialId": "xxxxxxxxx",
        "customUid": "xxxxxxxxxxxx"
    }],
    "win": {
        "tile": {
            "text1": "Text 1",
            "text2": "Text 2",
            "text3": "Text 3",
            "text4": "Text 4",
            "with_header": "true",
            "image": "https://domain.com/pic.png"
        },
        "options": {
            "expire": 36000
        }
    }
}
```

## Badge ("badge" object):

| Property | Type   | Description                                                                                                                                                                                                                                                                                                                                                                                                                  |
| -------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| badge    | string | <p>A number from 1 to 99. A value of 0 is equivalent to the glyph value "none" and will clear a badge.</p><p>Instead of a number, a badge can display one of a non-extensible set of status glyphs:</p><ul><li>activity</li><li>none</li><li>alarm</li><li>alert</li><li>attention</li><li>available</li><li>away</li><li>busy</li><li>error</li><li>newMessage</li><li>paused</li><li>playing</li><li>unavailable</li></ul> |

**Example**

```json
{
	"user_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
	"app_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
	"campaign_tag": "campaign name",
	"pack_id": "uniqueid1234",
	"audience": [{
		"token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
		"advertisingId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
		"serialId": "xxxxxxxxx",
		"customUid": "xxxxxxxxxxxx"
	}],
	"win": {
		"badge": {
			"badge": "99"
		}
	}
}
```

Additional settings of notification delivery and display ("options" object)

| Property | Type   | Description                                                                                                                                                                                                                                                                                                           |
| -------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| expire   | number | This option identifies the date when the tile or toast is no longer valid and can be discarded. It is possible to use either relative time in seconds passed since sending, or to specify an exact date in UNIX epoch format date expressed in seconds (UTC). Default value is 7 days (604800 seconds) after sending. |


# Raw Export

devtodev RAW export API

To use Raw data export API, you need to have an individual User API token, which can be found in [Space settings](/space-management#general).&#x20;

<figure><img src="/files/HRJPdvXbek97eioBXkhd" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
You’ll see the block with the User API token on the [space settings](/space-management#general) page only if your plan and access rights allow you to use devtodev API. You can reset User API token or create it again on the same page.
{% endhint %}

<figure><img src="/files/nbOD0cmEmJKoM2wiA2YY" alt=""><figcaption></figcaption></figure>

If some applications of the space are inaccessible to the owner of User API token, the owner will not have access to data export for these applications. However,  access to specific metrics is not extended to access to raw data.&#x20;

{% hint style="info" %}
Please note: if you have several spaces, the individual User API token is different for each space.
{% endhint %}

## The pattern of communication with devtodev RAW data export API

The process of getting data consists of three steps:

* job assignment
* getting the status of performance
* getting the report file by link

Let’s have a look at these steps.

### Job assignment

This step is the most important, and its description is the longest.

Send assignment request to:

```html
​https://devtodev.com/api/v1/rawexport/setjob?user_token=USER_API_TOKEN
```

Where

* **user\_token** – individual User API token of the user. It could be sent with both GET and POST methods.
* **v1** – current version of API.&#x20;

The request content is sent with POST method in JSON format.

The body of the request contains the following properties:

| Property           | Type   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ------------------ | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| user\_token        | string | Individual user API token. It can be found on the space settings page. It is possible to send it with both POST and GET methods.                                                                                                                                                                                                                                                                                                                                                                                                                      |
| app\_id            | string | Application identificator. It can be found in the application settings in the Integration section.                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| start\_date        | number | Start date of the request interval. Attention! Data is sent in UNIX-time format. Don’t forget to add/subtract the number of correction seconds to get data according to your timezone.                                                                                                                                                                                                                                                                                                                                                                |
| end\_date          | number | End date of the request interval. Data is sent in UNIX-time format.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| user\_card\_filter | object | <p>User filter by user card properties. Allows you to upload events only for a specific group/cohort of users. The cohort is selected using this filter. <br><a href="#user-card-filtering">A detailed description of the possible filter options is below</a>.</p>                                                                                                                                                                                                                                                                                   |
| platform           | array  | <p><strong>Applicable only to cross-platform projects.</strong> </p><p>Filter by platform IDs. <br><a href="#filtering-by-platform">See detailed description below</a>.</p>                                                                                                                                                                                                                                                                                                                                                                           |
| events             | array  | <p>List of events to export. The element of the array can be in the following formats:</p><ul><li>Event identificator (string).  Have a look at the list of available events and their identificators.</li><li>Event group identificator (string).  Have a look at the list of available events and their identificators.</li><li>Object with the name of custom event where it is possible to assign alias and to add filters. Have a look at Custom events filtering.</li></ul><p>The empty array means the export of all the available events.</p> |

#### List of available events and their identifiers

Every Basic event in devtodev SDK has a corresponding two-letter identifier. In case of custom events, the name of the event is the identifier.&#x20;

You can also use group identifiers (start with @).

| Event ID              | Event name                                                        |
| --------------------- | ----------------------------------------------------------------- |
| lu                    | Level up                                                          |
| rp                    | Real Payment                                                      |
| tr                    | Tutorial step                                                     |
| ip                    | Ingame purchase (Virtual Currency Payment)                        |
| sc                    | Social network connect                                            |
| sp                    | Social network post                                               |
| gs                    | Sessions                                                          |
| uu                    | User update                                                       |
| ud                    | UDIDs                                                             |
| pe                    | Progression event                                                 |
| rg                    | Registration (install date)                                       |
| sbs                   | Subscription (all actions with subscriptions)                     |
| adrv                  | Ad impressions. Data from ad networks or the Ad Impression event. |
| @basic\_events        | All events below                                                  |
| sbs\_payments         | Subscription (payments only)                                      |
| Any custom event name | Custom event                                                      |
| @custom\_events       | All custom events                                                 |

#### Custom events filtering

If you’re interested in the export of custom events according to some specific conditions, you should set the objects with the following properties in the array of the event list:

<table><thead><tr><th width="133.33333333333331">Property</th><th width="119">Type</th><th>Description</th></tr></thead><tbody><tr><td>name</td><td>string</td><td>Name of custom event</td></tr><tr><td>filters</td><td>object</td><td><p>Object with the filtering conditions.</p><p>Names of the object properties correspond with the name of custom event parameters. It is possible to apply filters to each of them.</p><p>Example:   </p><p><code>"filters":{</code> <br>        <code>"paramName1":{</code> <br>                <code>"gt": 5 // Events where paramName1>5. Have a look at List of comparison operators.</code> <br>        <code>}</code> <br><code>}</code></p></td></tr><tr><td>alias</td><td>string</td><td>Name of file in the report where to put the event with filter. The default name is set in the “name” property. <br><strong>Attention!</strong> If you export several equal events with different filters, the “alias” property is obligatory. </td></tr></tbody></table>

#### **Filtering by platform**&#x20;

{% hint style="info" %}
**Only for** [**Cross-platform projects**](/getting-started/adding-an-app-to-the-space/cross-platform-application)
{% endhint %}

If you want to export data only for specific platforms, excluding the rest, use this filter.

You need to specify an array with the identifiers of the platforms you need. As a result, you will receive events that occurred only on these platforms during the reporting period. User information will include cross-platform (general) profile data, as well as platform-level user profiles for all platforms where the user was active.&#x20;

You can also use the value `"undefined"` as a platform identifier to get events that were received without specifying a platform.&#x20;

If you additionally specify the value `"@general"`, the exported user data will be limited to only cross-platform (general) user profile information.&#x20;

See example [below](#examples).

#### **User card filtering**

If you want to export events from a specific audience, you can filter them by user card properties. Only the events that occurred with the filtered users will be included in the exported data.&#x20;

{% hint style="warning" %}
**Caution!** Filtering uses the property values of the user card at the time of the data export request.
{% endhint %}

The names of the properties are the same as the column names in the "users" table of the project, which you can find in the SQL report.&#x20;

See example [below](#examples).

The table shows the fields by which filtering is possible:

<table><thead><tr><th width="206.33333333333331">Property</th><th width="104">Type</th><th>Description</th></tr></thead><tbody><tr><td>devtodevid</td><td>string</td><td>Unique identifier of the user which is used in devtodev and assigned to the user when he launches the app for the first time.</td></tr><tr><td>customuid</td><td>string</td><td>User ID, assigned by the developer.</td></tr><tr><td>idfa</td><td>string</td><td>iOS advertising identifier</td></tr><tr><td>idfv</td><td>string</td><td>iOS vendor identifier</td></tr><tr><td>advertisingid</td><td>string</td><td>Advertising ID (Android, Windows)</td></tr><tr><td>androidid</td><td>string</td><td>Android ID</td></tr><tr><td> </td><td></td><td></td></tr><tr><td>created</td><td>int</td><td>Unix timestamp (ms) when the user opened the app for the first time</td></tr><tr><td>lasttime</td><td>int</td><td>Unix timestamp (ms) of the last user payment</td></tr><tr><td> </td><td></td><td></td></tr><tr><td>publisher</td><td>string</td><td>Acquisition. The name of the publisher/ad network that led to the acquisition of the user</td></tr><tr><td>campaign</td><td>string</td><td>Acquisition. The name of the ad campaign that resulted in acquiring the user</td></tr><tr><td>placement</td><td>string</td><td>Acquisition. Place where the ad unit is located</td></tr><tr><td>ad</td><td>string</td><td>Acquisition. Name of ad unit/banner</td></tr><tr><td> </td><td></td><td></td></tr><tr><td>firstpaymentdate</td><td>int</td><td>Unix timestamp (ms) of the first user payment</td></tr><tr><td>lastpaymentdate</td><td>int</td><td>Unix timestamp (ms) of the last user payment</td></tr><tr><td>paymentcount</td><td>int</td><td>Number of payments that the user have made in the app</td></tr><tr><td>paymentsum</td><td>number</td><td>Total sum of payments in USD</td></tr><tr><td>sbsfirstpaymentdate</td><td>int</td><td>Unix timestamp of the first subscription payment date</td></tr><tr><td>sbspaymentcount</td><td>int</td><td>The number of subscription payments made by the user in the app</td></tr><tr><td>sbspaymentsum</td><td>number</td><td>Total amount of subscription payments in USD</td></tr><tr><td> </td><td></td><td></td></tr><tr><td>cheater</td><td>boolean</td><td>Whether the user has cheated or not (true/false)</td></tr><tr><td>tester</td><td>boolean</td><td>Whether the user is a tester or not (true/false)</td></tr><tr><td> </td><td></td><td></td></tr><tr><td>appversion</td><td>string</td><td>Current version of the app</td></tr><tr><td>firstappversion</td><td>string</td><td>App version installed by the user at the time of registration</td></tr><tr><td>sdkversion</td><td>string</td><td>Current version of devtodev SDK</td></tr><tr><td> </td><td></td><td></td></tr><tr><td>level</td><td>int</td><td>Current user level</td></tr><tr><td>locale</td><td>string</td><td>User/device language (ISO 639-1, ISO 639-2, ISO 639-3)</td></tr><tr><td>country</td><td>string</td><td>User/device country (ISO_3166-1_alpha-2)</td></tr><tr><td>timezoneoffset</td><td>int</td><td>User timezone offset from UTC in milliseconds</td></tr><tr><td>pushavailable</td><td>boolean</td><td>True if there is a push token in devtodev DB</td></tr><tr><td> </td><td></td><td></td></tr><tr><td>_AnyCustomUserPropertyName<br><br><em>The prefix "_" here should be used to indicate that this is a custom property</em></td><td>string, number, boolean</td><td>Custom user card property, assigned by the developer.</td></tr></tbody></table>

#### List of comparison operators which can be applied in filter

The filter can be described with an object, where the following comparison operators can be the properties:

| API operator | Math operator | Description           |
| ------------ | ------------- | --------------------- |
| gt           | >             | Greater than          |
| lt           | <             | Less than             |
| eq           | =             | Equal                 |
| gte          | >=            | Greater than or equal |
| lte          | <=            | Less than or equal    |
| neq          | !=            | Not equal             |
| eq           |               | In the list           |
| neq          |               | Not in the list       |

The empty object describes a filter which selects all the events where the parameter value is not null or an empty string.&#x20;

#### Examples

```
https://devtodev.com/api/v1/rawexport/setjob
```

POST

```json
{
	"user_token": "USER_API_TOKEN", // It is obligatory, if it’s not sent with GET.
	"app_id": "af0606ed-bbdc-065a-952c-0d92561f107c",  // Obligatory. Application identificator.
	"start_date": 1464709200,  // Obligatory. Unix time of export start date.
	"end_date": 1464739200,  // Obligatory. Unix time of export end date.
	"events": [ //Not obligatory. List of events to export.
		"tr", // Export all  Tutorial step events
		"customEvent1Name",  // Export all customEvent1Name events
        //{"name": "customEvent1Name"} - alternative way with the same result
		{
			"name": "customEvent2Name",
			"filters": {  // Object with filter conditions
				"paramName1": {
					// 5<paramName1<=10 and paramName1!=8
					"lte": 10,
					"gt": 5,
					"neq": 8
				},
				"paramName2": {
					//Parameter paramName2 is equal to the one of elements.
					"eq": ["a","b","c"] 
				},
				"paramName3": {} // paramName3 is not empty.
			}
		}
	],
	"user_card_filter": { //user card properties filter object
		"paramName1": { //name of preset or custom user card parameter
			"lte": 10,
			"gt": 5,
			"neq": 8
		},
		"paramName2": { //name of preset or custom user card parameter
			"eq": ["a", "b", "c"]
		}
	},
    "platform": ["AS4p7","GP97g","@general"]
}
```

Or another way with event group identifier.

```json
{
	"user_token": "USER_API_TOKEN",
	"app_id": "af0606ed-bbdc-065a-952c-0d92561f107c",
	"start_date": 1464709200,
	"end_date": 1464739200,
	"events": ["@custom_events"] //Export all custom events
}
```

#### The format of the response to the job assignment

The response in case of successful start execution:

```json
{
	"status_code": 200,
	"data": "JOB_ID"
}
```

Where

* **status\_code** – is the HTTP status code;
* **data** – an identifier assigned to the job.

If you get this response, you can go to the next stage – [request of job status](#getting-the-status-of-performance) (else go to [Error handling](#error-handling)).

### Getting the status of performance

With the job identifier, you can request its current status using the following request:

```
https://devtodev.com/api/v1/rawexport/getprogress?user_token=USER_API_TOKEN
```

The body of the request can contain the following properties:

| Property    | Type   | Description                                                                                                                      |
| ----------- | ------ | -------------------------------------------------------------------------------------------------------------------------------- |
| user\_token | string | Individual user API token. It can be found on the space settings page. It is possible to send it with both POST and GET methods. |
| job\_id     | string | Job identificator which can be got from the response of job assignment.                                                          |

#### The format of the response to the request for status

The response to the request for status can be one of the following:

* The job is in the queue
* The job is in progress
* The job is done
* Error of job performance

Let’s explore every variant.

#### The job is in the queue

```json
{  
	"status_code":202,
	"data":{  
		"status":"pending",
		"percent_complete":0
	}
}
```

#### The job is in progress

```json
{  
	"status_code":200,
	"data":{  
		"status":"running",
		"percent_complete":10
	}
}
```

#### The job is done

```json
{  
	"status_code":201,
	"data":{  
		"status":"complete",
		"percent_complete":100,
		"result":{  
			"msg":"Report file is ready",
			"format":"csv",  // Format of report files in archive
			"url":"https://devtodev.com/api/v1/rawexport/download/?job_id=JOB_ID&user_token=USER_API_TOKEN",
			"ttl": 100500, // Report lifetime from the end of the performance to the removal
			"expire":123434534534  // Date when the report will be removed (in UNIX-time format)
		}
	}
}
```

To get the report, use the link in the URL property.

The report file is a zip-archive with files in `.CSV` format. Every file contains data about one event and/or alias (which was set in the filter in job assignment).

If there are no events according to the query conditions, the response will be the following:

```json
{  
	"status_code":200,
	"data":{  
		"status":"complete",
		"percent_complete":100,
		"result":{  
			"msg":"Unfortunately the result of your request is empty. Please try to change the report conditions."
		}
	}
}
```

## Error handling

The error can arise either at the moment of job assignment or at the moment of checking the status.&#x20;

In case there is an error, a response is made in the following format:&#x20;

```json
{
    "status_code": 400,
    "errors": [{
        "code": 3,
        "msg": "Error description"
    }]
}
```

where

* **status\_code** *(number)* – a general status of an error
* **errors** (array) – an array of error descriptions
* **code** *(number)* – the exact code of an error from the table of errors
* **msg** *(string)* – a brief description of an error

The list of possible errors is given in a table.

<table><thead><tr><th width="131.96484375">Status code</th><th width="95.984375">Code</th><th>Value of "msg" field</th><th>Error description</th></tr></thead><tbody><tr><td>400</td><td>2</td><td>Request body is empty</td><td>Empty body of the request. There is no POST data in the request.</td></tr><tr><td>400</td><td>3</td><td>Malformed json</td><td>JSON error in the body of the equest. Fix the formatting error.</td></tr><tr><td>401</td><td>11</td><td>Authorization error. Wrong user token %user_token value%</td><td>Authorization error. The set token is wrong. “User_token” field. User API token.</td></tr><tr><td>401</td><td>12</td><td><p>Authorization error.</p><p>User_token is not set.</p></td><td>Authorization error. “User_token” field is absent. User API token should be set either as a parameter in GET string of request or in POST body of request.</td></tr><tr><td>400</td><td>6</td><td>Invalid app id %app id value%</td><td>The requested project cannot be found. Unknown application. This error can arise when a user makes a mistake with the App ID or when the application with this ID was removed.</td></tr><tr><td>403</td><td>13</td><td>Access denied. You have no access to the app  %app id value%</td><td>Access error. User has no access to this application.</td></tr><tr><td>403</td><td>14</td><td>Access denied. You have no access to the report file %file id value%</td><td>Access error. User has no access to this file. This error can arise if you have no access to the application used in the previously created request.</td></tr><tr><td>403</td><td>15</td><td>Access denied. You have no access to API.</td><td>Access error. No access to User API token. This error can arise when the access rights were changed (in consequence of changing the user role or tariff plan)</td></tr><tr><td>403</td><td>16</td><td>Access denied. You have no access to RAW data export API.</td><td>Access error. This error can arise when your user role has no rights to RAW data export API or price plan.</td></tr><tr><td>400</td><td>4</td><td>Field not found: %field_name%</td><td>An obligatory field can not be found. You need to complement the request with this field.</td></tr><tr><td>403</td><td>17</td><td>Quota exceeded. %% concurrent requests per user has been reached.</td><td>The limit of simultaneous jobs per user is exceeded. Wait until one of the jobs will be finished and repeat the request.</td></tr><tr><td>403</td><td>24</td><td>Access denied. Raw Data API is unavailable due to plan or billing restrictions. Please contact your manager or our support team for details.</td><td>Raw Data API is unavailable due to plan or billing restrictions. Please contact your manager or our support team for details.</td></tr><tr><td>429</td><td>18</td><td>Too many requests. %% requests per day per user quota has been exhausted.</td><td>The daily limit of the user is exceeded. You can send the next request at the beginning of the next day.</td></tr><tr><td>429</td><td>19</td><td>Too many requests. %% requests per day per space quota has been exhausted.</td><td>The daily space limit is exceeded. You can send the next request at the beginning of the next day.</td></tr><tr><td>429</td><td>20</td><td>Too many requests. One request per %% seconds per user quota has been exhausted.</td><td>The limit of request frequency is exceeded. You have to wait for the time set in the error text.</td></tr><tr><td>404</td><td>21</td><td>File not found. The requested report file had been expired and was deleted.</td><td>The requested file can not be found. This can be when the file was removed due to the end of its lifetime. You have to repeat the job assignment and wait for the new file.</td></tr><tr><td>400</td><td>10</td><td><p>Incorrect report time frame interval:</p><p>start_date and end_date can not be earlier than 90 days from now.</p></td><td><p>Incorrect report time frame interval.</p><p>Both start and end date should be later than 90 days from now.</p></td></tr><tr><td>400</td><td>22</td><td>The job_id you requested is not found.</td><td>The job_id you requested is not found or the job was done and the report lifetime is exceeded.</td></tr><tr><td>400</td><td>5</td><td>Field %field_name% has type %received_type% but %expected_type% expected</td><td>The data type is not equal to the expected data type. Check the correspondence of the request with the format set in the documentation. Possible data types: boolean, integer, float, number (integer+float), string.</td></tr><tr><td>400</td><td>7</td><td>Unknown format: %format%</td><td>You have set the unknown export file format.</td></tr><tr><td>400</td><td>9</td><td>Event alias duplicated: %alias%</td><td>The event alias is duplicated. There should be no equal custom events without alias and no equal aliases in the request.</td></tr><tr><td>400</td><td>23</td><td>Incorrect report time frame:<br>the period for which data is exported cannot exceed 31 days.</td><td>You are not permitted to export data covering a period longer than 31 days in a single query.</td></tr><tr><td>400</td><td>8</td><td>Unknown event: %event_name%</td><td>Unknown event. This error can raise when the event is not in @basic_event group or the event doesn’t exist or the event is blocked custom event.</td></tr><tr><td>500</td><td>1</td><td>Unknown Error</td><td>Unknown error. Please contact devtodev technical support.</td></tr></tbody></table>

## Limitations

There are the following limitations to devtodev Raw data export API:

| Limitations                                          | Value     |
| ---------------------------------------------------- | --------- |
| Max number of job assignments per 24 hours per space | 100       |
| Max number of job assignments per 24 hours per user  | 50        |
| Max number of simultaneous job assignments per user  | 5         |
| Max timeout between user job assignments             | 5 seconds |
| Max time to perform one job                          | -         |
| Max lifetime of the report file                      | 24 hours  |
| Max number of report file downloads                  | 10        |

## List of available events

Below you can find the description of each table for events available for export.

### Users

This file is always attached to the exported file. The table contains a list of users who performed the exported events. It also contains their characteristics available at the time of the export.

<table><thead><tr><th>Column name</th><th>Description</th><th data-hidden></th></tr></thead><tbody><tr><td>devtodev ID</td><td>Numeric user identifier in the projects’s Users table</td><td></td></tr><tr><td>platform</td><td>Platform ID</td><td></td></tr><tr><td>Main ID</td><td>Main user identifier</td><td></td></tr><tr><td>Created</td><td>User registration date. Unix timestamp</td><td></td></tr><tr><td>Paying</td><td>Flags payers</td><td></td></tr><tr><td>Cheater</td><td>Flags cheaters</td><td></td></tr><tr><td>Tester</td><td>Flags testers</td><td></td></tr><tr><td>Level</td><td>Current user level</td><td></td></tr><tr><td>AppVersion</td><td>Current app version</td><td></td></tr><tr><td>Language</td><td>User’s device locale</td><td></td></tr><tr><td>Country</td><td>User country (set by IP address)</td><td></td></tr><tr><td>Device manufacturer</td><td>Device manufacturer</td><td></td></tr><tr><td>Device name</td><td>Device trademark name</td><td></td></tr><tr><td>Crossplatform User ID</td><td>User ID set by developer</td><td></td></tr><tr><td>Channel</td><td>Acquisition. User acquisition channel</td><td></td></tr><tr><td>InstallSource</td><td>Android. Android installer bundle</td><td></td></tr><tr><td>User agent</td><td>User agent</td><td></td></tr><tr><td>Screen resolution</td><td>Screen resolution of the device or app’s workspace</td><td></td></tr><tr><td>OS version</td><td>Version of the operating system</td><td></td></tr><tr><td>IDFV</td><td>iOS vendor identifier</td><td></td></tr><tr><td>IDFA</td><td>iOS advertising identifier</td><td></td></tr><tr><td>OPEN_UDID</td><td>OpenUDID</td><td></td></tr><tr><td>username</td><td>User name. Preset using the User card</td><td></td></tr><tr><td>useremail</td><td>User email. Preset using the User card</td><td></td></tr><tr><td>userphoto</td><td>User photo url. Preset using the User card</td><td></td></tr><tr><td>userphone</td><td>User telephone number. Preset using the User card</td><td></td></tr><tr><td>AdCampaign</td><td>Acquisition. The name of the ad campaign that resulted in acquiring the user</td><td></td></tr><tr><td>Time zone offset</td><td>User timezone offset from UTC in milliseconds</td><td></td></tr><tr><td>OsVersion</td><td>Version of the user’s operating system</td><td></td></tr><tr><td>segments</td><td>List of user custom segments containing this user. Segments separated by comma</td><td></td></tr><tr><td>Agency</td><td>Acquisition. Ad mediator name (Sub-publisher)</td><td></td></tr><tr><td>Keyword</td><td>Acquisition. Ad keywords and/or keywords that led to install</td><td></td></tr><tr><td>Placement</td><td>Acquisition. Place where the ad unit is located</td><td></td></tr><tr><td>Site</td><td>Acquisition. Website or app where the ad was placed</td><td></td></tr><tr><td>Ad group</td><td>Acquisition. Name of ad group</td><td></td></tr><tr><td>Ad</td><td>Acquisition. Name of ad unit/banner</td><td></td></tr><tr><td>segments</td><td>Comma separated segment names. The state at the time the data was exported.</td><td></td></tr><tr><td>abtests</td><td>Comma separated names of AB test groups. The state at the time the data was exported.</td><td></td></tr><tr><td>Custom property field name 1</td><td>User card custom property</td><td></td></tr><tr><td>Custom property field name N</td><td>User card custom property</td><td></td></tr></tbody></table>

### Sessions

Event code `gs`.

This file contains entries on session starts and user activity times for the selected export period. The table contains session starts (rows with filled **`session_starts`** field) and time when the app was in focus (rows with filled **activity\_duration** field).

<table><thead><tr><th>Column name</th><th>Description</th><th data-hidden></th></tr></thead><tbody><tr><td>devtodev ID</td><td>Numeric user identifier in the project’s Users table</td><td></td></tr><tr><td>platform</td><td>Platform ID</td><td></td></tr><tr><td>time</td><td>Event (session start or end of activity) start date. Unix timestamp in milliseconds</td><td></td></tr><tr><td>session_starts</td><td>Session start. Marked as 1 when session started.</td><td></td></tr><tr><td>activity_duration</td><td>Duration of user activity (time the app was in focus)</td><td></td></tr><tr><td>isTester</td><td>At the time of event start the user was a Tester</td><td></td></tr><tr><td>cheat</td><td>At the time of event start the user was a Cheater</td><td></td></tr><tr><td>level</td><td>User level at the time of event start</td><td></td></tr><tr><td>install_date</td><td>User registration date. Unix timestamp</td><td></td></tr><tr><td>app_version</td><td>Application version. The data at the moment the event was generated.</td><td></td></tr><tr><td>segments</td><td>Comma separated segment names. The data at the moment the event was generated.</td><td></td></tr><tr><td>abtests</td><td>Comma separated names of AB test groups. The data at the moment the event was generated.</td><td></td></tr></tbody></table>

### IngamePurchase (Virtual Currency Payment)

Event code `ip`.

This file contains entries on virtual purchases in the app.

<table><thead><tr><th>Column name</th><th>Description</th><th data-hidden></th><th data-hidden></th></tr></thead><tbody><tr><td>devtodev ID</td><td>Numeric user identifier in the project's Users table</td><td></td><td></td></tr><tr><td>platform</td><td>Platform ID</td><td></td><td></td></tr><tr><td>time</td><td>Event (session start or end of activity) start date. Unix timestamp in milliseconds</td><td></td><td></td></tr><tr><td>level</td><td>User level at the time of event start</td><td></td><td></td></tr><tr><td>item_type</td><td>Item category</td><td></td><td></td></tr><tr><td>item</td><td>Item name</td><td></td><td></td></tr><tr><td>count</td><td>Amount of items the user bought</td><td></td><td></td></tr><tr><td>cheat</td><td>At the time of event start the user was a Cheater</td><td></td><td></td></tr><tr><td>isTester</td><td>At the time of event start the user was a Tester</td><td></td><td></td></tr><tr><td>install_date</td><td>User registration date. Unix timestamp</td><td></td><td></td></tr><tr><td>Virtual currency name 1</td><td>Amount of virtual currency spent on the item (overall)</td><td></td><td></td></tr><tr><td>Virtual currency name N</td><td>Amount of virtual currency spent on the item (overall)</td><td></td><td></td></tr><tr><td>app_version</td><td>Application version. The data at the moment the event was generated.</td><td></td><td></td></tr><tr><td>segments</td><td>Comma separated segment names. The data at the moment the event was generated.</td><td></td><td></td></tr><tr><td>abtests</td><td>Comma separated names of AB test groups. The data at the moment the event was generated.</td><td></td><td></td></tr></tbody></table>

### Install

Event code `rg`.

A user registration event used in the devtodev database. Usually, the registration date is the same as the date of the first launch of the app.

| Column name   | Description                                          |
| ------------- | ---------------------------------------------------- |
| devtodev ID   | Numeric user identifier in the project’s Users table |
| platform      | Platform ID                                          |
| install\_date | User registration date. Unix timestamp               |

### LevelUp

Event code `lu`.

An event for when the user reaches a certain game level.

| Column name   | Description                                                                                                                                                                                            |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| devtodev ID   | Numeric user identifier in the project’s Users table                                                                                                                                                   |
| platform      | Platform ID                                                                                                                                                                                            |
| time          | Event (session start or end of activity) start date. Unix timestamp in milliseconds                                                                                                                    |
| level         | User level at the time of event start                                                                                                                                                                  |
| isTester      | At the time of event start the user was a Tester                                                                                                                                                       |
| cheat         | At the time of event start the user was a Cheater                                                                                                                                                      |
| install\_date | User registration date. Unix timestamp                                                                                                                                                                 |
| spent         | <p>Amount of virtual currency spent by user on the previous level. List of currency names and amounts separated by comma. </p><p><strong>Example</strong>: “Coins: 90, Gold: 10”</p>                   |
| earned        | <p>Amount of virtual currency earned by user on the previous level. List of currency names and amounts separated by comma. </p><p><strong>Example</strong>: “Coins: 90, Gold: 10</p>                   |
| balance       | <p>Amount of virtual currency on user balance at the time of reaching a new level. List of currency names and amounts separated by comma. </p><p><strong>Example</strong>: “Coins: 90, Gold: 10</p>    |
| bought        | <p>Amount of virtual currency bought by user for real currency on the previous level. List of currency names and amounts separated by comma. </p><p><strong>Example</strong>: “Coins: 90, Gold: 10</p> |
| app\_version  | Application version. The data at the moment the event was generated.                                                                                                                                   |
| segments      | Comma separated segment names. The data at the moment the event was generated.                                                                                                                         |
| abtests       | Comma separated names of AB test groups. The data at the moment the event was generated.                                                                                                               |

### Payments

Event code `rp`.

List of user transactions. Purchases for real currency.

| Column name       | Description                                                                                                                                                                                        |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| devtodev ID       | Numeric user identifier in the project’s Users table                                                                                                                                               |
| platform          | Platform ID                                                                                                                                                                                        |
| date              | Event (session start or end of activity) start date. Unix timestamp in milliseconds                                                                                                                |
| level             | User level at the time of event start                                                                                                                                                              |
| transaction\_id   | Unique transaction identifier                                                                                                                                                                      |
| transaction\_name | Item name or SKU                                                                                                                                                                                   |
| amount\_in\_usd   | <p>Amount of real currency spent by user. Converted into USD using exchange rate at the moment the event is received by the devtodev server. <br>Value is negative if transaction is a refund.</p> |
| refund            | <p>Is transaction a refund or not. <br>True/False value. <br>If true, amount is negative.</p>                                                                                                      |
| status            | Is transaction valid or not                                                                                                                                                                        |
| install\_date     | User registration date. Unix timestamp                                                                                                                                                             |
| isTester          | At the time of event start the user was a Tester                                                                                                                                                   |
| cheat             | At the time of event start the user was a Cheater                                                                                                                                                  |
| app\_version      | Application version. The data at the moment the event was generated.                                                                                                                               |
| segments          | Comma separated segment names. The data at the moment the event was generated.                                                                                                                     |
| abtests           | Comma separated names of AB test groups. The data at the moment the event was generated.                                                                                                           |

### Progression

Event code `pe`.

This file contains a list of triggered **Progression** events.

| Column name    | Description                                                                                                                                                                                              |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| devtodev ID    | Numeric user identifier in the project’s Users table                                                                                                                                                     |
| platform       | Platform ID                                                                                                                                                                                              |
| time           | Event (session start or end of activity) start date. Unix timestamp in milliseconds                                                                                                                      |
| level          | User level at the time of event start                                                                                                                                                                    |
| location       | Game location name                                                                                                                                                                                       |
| success        | Is location successfully completed (true/false)                                                                                                                                                          |
| prev\_location | Name of the previous location                                                                                                                                                                            |
| duration       | Time spent on completing the location                                                                                                                                                                    |
| difficulty     | Location difficulty (numeric)                                                                                                                                                                            |
| isTester       | At the time of event start the user was a Tester                                                                                                                                                         |
| cheat          | At the time of event start the user was a Cheater                                                                                                                                                        |
| install\_date  | User registration date. Unix timestamp                                                                                                                                                                   |
| spent          | <p>Amount of virtual currency or resources spent by user upon completing the location. List of currency names and amounts separated by comma. </p><p><strong>Example</strong>: “Coins: 90, Gold: 10</p>  |
| earned         | <p>Amount of virtual currency or resources earned by user upon completing the location. List of currency names and amounts separated by comma. </p><p><strong>Example</strong>: “Coins: 90, Gold: 10</p> |
| app\_version   | Application version. The data at the moment the event was generated.                                                                                                                                     |
| segments       | Comma separated segment names. The data at the moment the event was generated.                                                                                                                           |
| abtests        | Comma separated names of AB test groups. The data at the moment the event was generated.                                                                                                                 |

### SocialNetworkConnects

Event code `sc`.

Social media connection events.

| Column name   | Description                                                                              |
| ------------- | ---------------------------------------------------------------------------------------- |
| devtodev ID   | Numeric user identifier in the project’s Users table                                     |
| platform      | Platform ID                                                                              |
| time          | Event (session start or end of activity) start date. Unix timestamp in milliseconds      |
| level         | User level at the time of event start                                                    |
| social        | Name of social media                                                                     |
| isTester      | At the time of event start the user was a Tester                                         |
| cheat         | At the time of event start the user was a Cheater                                        |
| install\_date | User registration date. Unix timestamp                                                   |
| app\_version  | Application version. The data at the moment the event was generated.                     |
| segments      | Comma separated segment names. The data at the moment the event was generated.           |
| abtests       | Comma separated names of AB test groups. The data at the moment the event was generated. |

### SocialNetworkPosts

Event code `sp`.

Social media publication events.

| Column name   | Description                                                                              |
| ------------- | ---------------------------------------------------------------------------------------- |
| devtodev ID   | Numeric user identifier in the projest’s Users table                                     |
| platform      | Platform ID                                                                              |
| time          | Event (session start or end of activity) start date. Unix timestamp in milliseconds      |
| level         | User level at the time of event start                                                    |
| social        | Name of social media                                                                     |
| reason        | Reason for publication or title of publication                                           |
| isTester      | At the time of event start the user was a Tester                                         |
| cheat         | At the time of event start the user was a Cheater                                        |
| install\_date | User registration date. Unix timestamp                                                   |
| app\_version  | Application version. The data at the moment the event was generated.                     |
| segments      | Comma separated segment names. The data at the moment the event was generated.           |
| abtests       | Comma separated names of AB test groups. The data at the moment the event was generated. |

### SubscriptionPayments

Event code `sbs_payments`.

Subscription events: buying, renewal, and refund. This file contains only subscription events that impact revenue.

| Column name       | Description                                                                                                                              |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| devtodev ID       | Numeric user identifier in the project’s Users table                                                                                     |
| platform          | Platform ID                                                                                                                              |
| date              | Event (session start or end of activity) start date. Unix timestamp in milliseconds                                                      |
| level             | User level at the time of  event start                                                                                                   |
| transaction\_id   | Unique transaction identifier                                                                                                            |
| transaction\_name | Subscription name or SKU                                                                                                                 |
| amount\_in\_usd   | Amount of real currency spent by user. Converted into USD using exchange rate at the moment the event is received by the devtodev server |
| install\_date     | User registration date. Unix timestamp                                                                                                   |
| isTester          | At the time of event start the user was a Tester                                                                                         |
| cheat             | At the time of event start the user was a Cheater                                                                                        |
| app\_version      | Application version. The data at the moment the event was generated.                                                                     |
| segments          | Comma separated segment names. The data at the moment the event was generated.                                                           |
| abtests           | Comma separated names of AB test groups. The data at the moment the event was generated.                                                 |

### Subscriptions

Event code `sbs`.

All subscription events.

| Column name           | Description                                                                                                                                            |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| devtodev ID           | Numeric user identifier in the project’s Users table                                                                                                   |
| platform              | Platform ID                                                                                                                                            |
| date                  | Event (session start or end of activity) start date. Unix timestamp in milliseconds                                                                    |
| level                 | User level at the time of event start                                                                                                                  |
| transaction\_id       | Unique transaction identifier                                                                                                                          |
| transaction\_name     | Subscription name or SKU                                                                                                                               |
| action                | <p>An action to subscription. <strong>Examples</strong>: </p><p>purchased </p><p>changed renewal status </p><p>changed renewal pref </p><p>renewed</p> |
| is\_trial             | Is subscription a trial or not                                                                                                                         |
| is\_payment\_received | Is payment successfully received or not                                                                                                                |
| started\_at           | Subscription start date                                                                                                                                |
| expired\_at           | Subscription end date                                                                                                                                  |
| amount\_in\_usd       | Amount of real currency spent by user. Converted into USD using exchange rate at the moment the event is received by the devtodev server               |
| install\_date         | User registration date. Unix timestamp                                                                                                                 |
| isTester              | At the time of event start the user was a Tester                                                                                                       |
| cheat                 | At the time of event start the user was a Cheater                                                                                                      |
| app\_version          | Application version. The data at the moment the event was generated.                                                                                   |
| segments              | Comma separated segment names. The data at the moment the event was generated.                                                                         |
| abtests               | Comma separated names of AB test groups. The data at the moment the event was generated.                                                               |

### Tutorial

Event code `tr`.

Tutorial steps events.

| Column name     | Description                                                                                                                                                                                                           |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| devtodev ID     | Numeric user identifier in the project’s Users table                                                                                                                                                                  |
| platform        | Platform ID                                                                                                                                                                                                           |
| time            | Event (session start or end of activity) start date. Unix timestamp in milliseconds                                                                                                                                   |
| level           | User level at the time of event start                                                                                                                                                                                 |
| complete\_state | <p>Number of the completed step of the tutorial. </p><p>Also has predefined values: </p><p>0 – the user skipped the tutorial </p><p>-1 – the user started the tutorial </p><p>-2 – the user finished the tutorial</p> |
| isTester        | At the time of event start the user was a Tester                                                                                                                                                                      |
| cheat           | At the time of event start the user was a Cheater                                                                                                                                                                     |
| install\_date   | User registration date. Unix timestamp                                                                                                                                                                                |
| app\_version    | Application version. The data at the moment the event was generated.                                                                                                                                                  |
| segments        | Comma separated segment names. The data at the moment the event was generated.                                                                                                                                        |
| abtests         | Comma separated names of AB test groups. The data at the moment the event was generated.                                                                                                                              |

### **UDIDs**

Event code `ud`.

This file contains information about changes to user and device identifiers during the selected export period.

| Column name           | Description                                          |
| --------------------- | ---------------------------------------------------- |
| devtodev ID           | Numeric user identifier in the project’s Users table |
| platform              | Platform ID                                          |
| Main ID               | Main user identifier                                 |
| IDFV                  | iOS vendor identifier                                |
| IDFA                  | iOS advertising identifier                           |
| Crossplatform User ID | User ID set by developer                             |
| Push token            | User push token                                      |

### **UserUpdate**

Event code `uu`.

This file contains data on user characteristics and device updates for the selected export period.

| Column name           | Description                                                    |
| --------------------- | -------------------------------------------------------------- |
| devtodev ID           | Numeric user identifier in the project’s Users table           |
| platform              | Platform ID                                                    |
| Main ID               | Main user identifier                                           |
| IDFV                  | iOS vendor identifier                                          |
| IDFA                  | iOS advertising identifier                                     |
| Crossplatform User ID | User ID set by developer                                       |
| logged                | Date of user’s last activity                                   |
| level                 | User level at the time of event start                          |
| language              | User device locale                                             |
| country               | User country (from IP address)                                 |
| sdk\_version          | SDK version at the time of data update                         |
| app\_version          | App version at the time of data update                         |
| created               | User registration date. Unix timestamp                         |
| paying                | Is user a payer or not                                         |
| device\_name          | Device trademark name                                          |
| cheater               | At the time of event start the user was a Cheater              |
| osversion             | Version of the operating system at the time of the data update |

### **AdImpression**

Event code `adrv`.

This file contains the app’s ad impression events. Data from ad networks or the Ad Impression event.

| Column name   | Description                                                                         |
| ------------- | ----------------------------------------------------------------------------------- |
| devtodev ID   | Numeric user identifier in the project’s Users table                                |
| platform      | Platform ID                                                                         |
| date          | Event (session start or end of activity) start date. Unix timestamp in milliseconds |
| level         | User level at the time of event start                                               |
| placement     | Place where the ad unit is located                                                  |
| network       | Name of ad network responsible for placement                                        |
| unit          | Name of ad unit/banner                                                              |
| source        | Ad impression data source                                                           |
| revenue       | Ad impression revenue in USD                                                        |
| install\_date | User registration date. Unix timestamp                                              |
| isTester      | At the time of event start the user was a Tester                                    |
| cheat         | At the time of event start the user was a Cheater                                   |

### CustomEvent.{EventName}

This file contains custom events with {EventName} name.

| Column name                        | Description                                                                              |
| ---------------------------------- | ---------------------------------------------------------------------------------------- |
| devtodev ID                        | Numeric user identifier in the project’s Users table                                     |
| platform                           | Platform ID                                                                              |
| date                               | Event (session start or end of activity) start date. Unix timestamp in milliseconds      |
| level                              | User level at the time of event start                                                    |
| isTester                           | At the time of event start the user was a Tester                                         |
| cheat                              | At the time of event start the user was a Cheater                                        |
| install\_date                      | User registration date. Unix timestamp                                                   |
| app\_version                       | Application version. The data at the moment the event was generated.                     |
| segments                           | Comma separated segment names. The data at the moment the event was generated.           |
| abtests                            | Comma separated names of AB test groups. The data at the moment the event was generated. |
| custom parameter name 1            | Custom parameter value (string, number or boolean)                                       |
| custom parameter name N (up to 30) | Custom parameter value (string, number or boolean)                                       |


# Labels API

## Obtaining the User API token

In order to be able to use Labels API you must have an individual User API token, which can be found in space settings in devtodev system.&#x20;

<figure><img src="/files/HRJPdvXbek97eioBXkhd" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
You will be able to see the interface unit with User API token in space settings only in case your price plan and access rights (role) allow you to use devtodev API. You can discard User API token or create it again on the same page.&#x20;
{% endhint %}

<figure><img src="/files/nbOD0cmEmJKoM2wiA2YY" alt=""><figcaption></figcaption></figure>

Pay your attention that if you use several spaces, each of them will have an individual *User API token.*&#x20;

## Obtaining the list of categories

The result of the request is the obtaining of the list of all existing labels categories for the app.&#x20;

Request:

```
https://www.devtodev.com/api/v1/labels/categories/get
```

POST

```json
{
	"user_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", // devtodev user API token
	"app_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"  // devtodev App ID
}
```

Response:

```json
{
	"status_code": 200,
	"data": {
		"status": "complete",
		"categories": [
			"category name",
			"category name 2"
		]
	}
}
```

## Removing categories

The request is used to remove or rename a category. The category can be removed only if its full name is specified. One request can remove only one category. In case the parameter ***move\_labels\_to\_category*** is specified, labels that belong to the category that is going to be removed will be transfered to the specified category. If the specified category doesn't exist, it will be created. If  ***move\_labels\_to\_category*** is not specified, all the labels that belong to the category will be removed. &#x20;

Request:

```
https://www.devtodev.com/api/v1/labels/categories/delete
```

POST

```json
{
	"user_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
	"app_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
	"category": "category to be removed",
	"move_labels_to_category": "category to which the labels will be moved"
}
```

Response:

```json
{
	"status_code": 200,
	"data": {
		"status": "deleted",
		"category": "category to be removed"
	}
}
```

## Obtaining the list of labels

The result of the request is the obtaining of the list of labels that are presented as objects.&#x20;

You can use filters to get some specific labels you need. In case no filter is specified, you will receive the whole list of labels. It is possible to use any combination of fields in a filter. &#x20;

Request:

```
https://www.devtodev.com/api/v1/labels/get
```

POST

```json
{
	"user_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
	"app_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
	"filters": {
		"id": {
			"in": [100500, 100501]
		},
		"category": {
			"in": ["category name", "category name 2"]
		},
		"start_date": {
			"gte": 1478625206
		},
		"end_date": {
			"lte": 1478625206
		}
	}
}
```

Response:

```json
{
	"status_code": 200,
	"data": {
		"status": "complete",
		"labels": [{
			"id": 100500,
			"name": "label name",
			"description": "short label description",
			"category": "category name",
			"start_date": 1478625206,
			"end_date": 1478625206
		}]
	}
}
```

### List of comparison operators that can be applied in a filter

The filter can be described with an object, where the following comparison operators can be the properties:

| API operator | Math operator | Description           |
| ------------ | ------------- | --------------------- |
| gt           | >             | Greater than          |
| lt           | <             | Less than             |
| eq           | =             | Equal                 |
| gte          | >=            | Greater than or equal |
| lte          | <=            | Less than or equal    |
| neq          | !=            | Not equal             |
| eq           |               | In the list           |
| neq          |               | Not in the list       |

The empty object describes the filter that selects all the events in which the parameter value is not null or empty string.

## Adding labels

In case the specified category doesn't exist, it will be created. If string values of fields exceed the maximum, they will be cut off.&#x20;

If you need to specify an event without time length, ***start\_date*** must be equal to ***end\_date*** or ***end\_date*** must not be specified.&#x20;

Request:

```
https://www.devtodev.com/api/v1/labels/add
```

POST

```json
{
	"user_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
	"app_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
	"labels": [{
		"name": "label name",
		"description": "short label description",
		"category": "category name",
		"start_date": 1478625206,
		"end_date": 1478625206
	}]
}
```

Response:

```json
{
	"status_code": 201,
	"data": {
		"status": "created",
		"labels": [{
			"id": 100500,
			"name": "label name",
			"description": "short label description",
			"category": "category name",
			"start_date": 1478625206,
			"end_date": 1478625206
		}]
	}
}
```

## Editing labels&#x20;

To edit a label you need to specify its identifier (***id***). All the specified fields for the label will be replaced. In case the specified category doesn't exist, it will be created.

If it is necessary to change the dates of an event, you must specify ***start\_date***. It is impossible to specify ***end\_date*** without specifying ***start\_date***. If the label is used in relation to a continuous event (***start\_data*** and ***end\_date*** are different timestamps), but in an edited version only the start\_date is received,  the label becomes a point that characterizes an instant event.

Request:

```
https://www.devtodev.com/api/v1/labels/edit
```

POST

```json
{
	"user_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
	"app_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
	"id": 100500,
	"name": "label name",
	"description": "short label description",
	"category": "category name",
	"start_date": 1478625206,
	"end_date": 1478625206
}
```

Response:

```json
{
	"status_code": 200,
	"data": {
		"status": "complete",
		"labels": [{
			"id": 100500,
			"name": "label name",
			"description": "short label description",
			"category": "category name",
			"start_date": 1478625206,
			"end_date": 1478625206
		}]
	}
}
```

## Deleting labels

The request is used to delete one or several labels. To delete a label its identifier is required. The request without specified identifiers is not valid.

Request:

```
https://www.devtodev.com/api/v1/labels/delete
```

POST

```json
{
	"user_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
	"app_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
	"ids": [100500, 100501]
}
```

Response:

```json
{
	"status_code": 200,
	"data": {
		"status": "deleted",
		"labels": [{
			"id": 100500,
			"name": "label name",
			"description": "short label description",
			"category": "category name",
			"start_date": 1478625206,
			"end_date": 1478625206
		}]
	}
}
```

Error handling

In case there is an error in a request, the response is made in the following format:

```json
{
	"status_code": 500,
	"errors": [{
		"code": 3,
		"msg": "Malformed json"
	}]
}
```

where

* status\_code (number) - the general status of an error
* errors (array) - an array of error descriptions
* code (number) - the exact code of an error from the table of errors
* msg (string) - a brief description of an error

The list of possible errors is given in a table.

### List of possible errors

| Status code | Code | Value of "msg" field                                                                    | Error description                                                                                                                                                                                                                     |
| ----------- | ---- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 500         | 1    | Unknown Error                                                                           | Unknown error. In case it repeats, please contact technical support.                                                                                                                                                                  |
| 400         | 2    | Request body is empty                                                                   | Request body is empty. There is no POST data in the request.                                                                                                                                                                          |
| 400         | 3    | Malformed json                                                                          | There is an error of JSON format in the request body. Fix the error of format.                                                                                                                                                        |
| 400         | 4    | Field not found: %field\_name%                                                          | The required field is not specified in the request. Please add it to the request.                                                                                                                                                     |
| 400         | 5    | Field %field\_name% has type %received\_type% but %expected\_type% expected             | The type of data doesn't match the expected type. Follow the recommendation and change the data type to the expected one. It is possible to use the following types of data: boolean, integer, float, number (integer+float), string. |
| 400         | 6    | Invalid app id %app id value%                                                           | The requested app is not found. The app is unknown. The error can appear in case the specified app identifier is wrong or when the app has been removed.                                                                              |
| 401         | 11   | Authorization error. Wrong user token %user\_token value%                               | Authorization error. The specified User API token is wrong. Check the value of the specified key.                                                                                                                                     |
| 401         | 12   | Authorization error. User\_token is not set.                                            | Authorization error. User API token is not specified in the request parameters. It is necessary to specify User API token ("user\_token" field) in every request.                                                                     |
| 403         | 13   | Access denied. You have no access to the app %app id value%                             | Access error. The owner specified in the User API token request has no access rights to the app specified in the same request.                                                                                                        |
| 403         | 15   | Access denied. You have no access to API.                                               | Access error. You have no access to devtodev API. The error can appear when User API  token owner's access rights to the service API are changed as a result of the change of the user's role or the change of the price plan.        |
| 403         | 23   | Access denied. You have no access to Labels API.                                        | Access error. The error can appear in case the User API token owner has no access to the  Labels API service due to the limitations of the user's role or limitations of the price plan of the space.                                 |
| 400         | 28   | Unexpected value for field %field%. Received value: %value%. Expected values: %values%. | Unexpected value for the field. Follow the recommendations and correct the request.                                                                                                                                                   |
| 400         | 29   | Unexpected field %field%                                                                | The request contains the field that is not specified in the documentation. The field must be excluded from the request.                                                                                                               |
| 400         | 30   | Invalid value for field %field%. Received value: %value%. Expected: %description%       | Invalid value has been attributed to the field. Follow the recommendations and correct the request.                                                                                                                                   |
| 404         | 34   | No results matched the request                                                          | The requested data is missing.                                                                                                                                                                                                        |
| 429         | 35   | Quota exceeded. %% new labels per day per application quota has been exceeded.          | The daily limit is exceeded. Tomorrow you will be able to add labels to the app again.                                                                                                                                                |
| 400         | 37   | New labels array should not contain more than %% elements.                              | The array of labels contains more than 30 elements. Reduce the number of labels created in one request.                                                                                                                               |
| 429         | 36   | Quota exceeded. %% new labels per day per user quota has been exceeded.                 | The daily limit is exceeded. You will be able to add a new set of labels tomorrow.                                                                                                                                                    |
| 400         | 38   | Labels feature is not available for cross-platform projects                             | The labels feature is not available for cross-platform projects. Labels can be applied only to ordinary apps or branch-apps of cross-platform projects.                                                                               |

## Limitations

### Limitations on the lenght of values

| Name                         | Field       | Limitation                |
| ---------------------------- | ----------- | ------------------------- |
| Label name                   | name        | not more than 10 symbols  |
| Short description of a label | description | not more than 512 symbols |
| Category name                | category    | not more than 30 symbols  |

### Limitations on the number of actions&#x20;

* Not more than 30 labels in one request on labels creation&#x20;
* Not more than 500 labels to be created in a calendar day for User API token&#x20;
* Not more than 200 labels to be created in a calendar day for an app


# Data API

Server API integration manual

{% hint style="danger" %}
This API version is deprecated, please use the [latest version](/integration/server-api/data-api-2.0).
{% endhint %}

## Request format

The request should be sent to:\
`https://api.devtodev.com/stat/v1/?api=ak-cDNRQl0Lypq4AOUrx8aGGMnmJT1FSebd` where:

* api  - individual devtodev API app key, which can be found on the page of app integration;
* v1 - the current version of the API aggregator.

All the transferred data must be in UTF8 encoding.

**Contents must be sent as POST in gzip.** \
The archive must contain JSON with one or more events for one or several users.&#x20;

The size of a package can't exсeed 1 MB before compression. Packages that exсeed this size can't be processed.&#x20;

If there are several events they must be formed inside a user object by type (name).

```json
{

    "abcd1234abcd" : {              // Main identifier (device or user ID)
        "prev" : "asdf2345234asdf", // Previous main identifier (device or user ID)
                                    // in case it has been changed 
        "userId":"",                // Additional identifier (it is used if there is an identifier
                                    // that is different from the main identifier.
                                    // For example, a cross-platform user identifier)
        "prevUserId":"";            // Previous additional identifier in case it has been changed
        "sc" :[                     // Event name
            {},                     // Parameters of the first event
            {},                     // Parameters of the second event
            …
        ],
        "gs" :[                     // Event name 
            {}                      // Event parameters
        ],
        …
    },
    "1234abc1234" : {               // Unique user ID
        "userID":"",                // Additional identifier (it is used if there is an identifier
                                    // that is different from the main identifier.
        "sc" :[                     // Event name
            {},                     // Parameters of the first event
            {},                     // Parameters of the second event
            …
        ],
        "gs" :[                     // Event name 
            {}                      // Event parameters
        ],
    },
}
```

## Response format

| HTTP Code | State                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 413       | Wrong size of the data package (exceeds the maximum)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| 400       | API key is absent                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| 401       | Wrong API key                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| 403       | Administrative restrictions on data received from a client                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| 400       | <p>The error of data unpacking</p><p><code>{</code> </p><p>    <code>"error\_message":"Wrong GZIP format"</code> </p><p><code>}</code></p>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| 400       | <p>The error of JSON format</p><p><code>{</code> </p><p>    <code>"error\_message":"Wrong JSON format"</code> </p><p><code>}</code></p>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| 200       | Package is received                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| 200       | <p>The package is received, but there are errors found during the validation of events syntax.</p><p>The errors found are specified in an answer in the following format:</p><p>1.</p><p><code>{</code> <br>    <code>"errors": {</code> <br>        <code>"rg": {</code> <br>            <code>"expected": "\[timestamp]",</code> <br>            <code>"received": \[</code> <br>                <code>"\[timestamps]",</code> <br>                <code>"\[timestaTTmp]"</code> <br>            <code>]</code><br>        <code>}</code><br>    <code>}</code>     <br><code>}</code><br>The message indicates that there have been received fields listed under the tag "received", but there are no fields that are listed under the tag "expected" among them. Such an event will not be processed. </p><p>2.</p><p><code>{</code> <br>    <code>"errors": {</code> <br>        <code>"rg": {</code> <br>            <code>"expected": "\[timestamp]",</code> <br>            <code>"useless": \[</code> <br>                <code>"\[timestamps]",</code> <br>                <code>"\[timestaTTmp]"</code> <br>            <code>]</code> <br>        <code>}</code> <br>    <code>}</code> <br><code>}</code></p><p>The message indicates that there have been received extra fields listed under the tag "useless", however, fields listed under the tag "expected" are enough. Such an event will be processed. </p> |
|           |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |

## The list of events

### Information about a source of user appearance&#x20;

(traffic source, referral)\
It is used when it is necessary to track data about a source of user appearance. The event is sent when an app is first launched by a user if a user came from a tracked source. Maximum of the fields from the following accessible data must be transferred.&#x20;

```json
"rf" : [
    {
        "timestamp" : 1386259227,   // Required field! The date of registration
        "publisher" : "",           // Required field! The source from which a user came - the name of 
                                    // An advertising platform (advertising network)
        "subpublisher" : "",        // In case a platform is an aggregator - the name of a platform 
                                    // From which a user was resold 
        "subad" : "",               // Specific banner from which a user came (for Facebook)
        "subadgroup" : "",          // The group of ads(for Facebook)
        "subcampaign" : "",         // The name of a campaign from which a user came 
        "subplacement" : "",        // Banner placement - its position on a page 
        "subsite" : "",             // Who placed a banner (an app/ a website where a banner is placed)
        "country" : "US",           // Required field! The country of a user (format ISO 3166-1 alpha-2)
        "currencyCode" : "USD",     // The currency of referral's cost (format ISO 4217)
        "cost" : 9.99,              // The cost of a referral in a specified currency 
    }
]
```

### Information about a user&#x20;

It is not sent in case there is some data missing. The event can't be submitted as a package.

{% hint style="warning" %}
Attention! We strongly recommend that you do not use these properties to transfer and store data that fits the definition of [personal data](https://gdpr-info.eu/issues/personal-data/)!
{% endhint %}

```json
"ui": [{
	"timestamp": 1386259227,
	"country": "GB", // The country of a user (format ISO 3166-1 alpha-2)
	"language": "en", // The language of a user (format ISO 639-1 (1998)
	"crossUid": "customuserid", // Custom user ID
	"ip": "127.0.0.1", // IP
	"carrier": "Beeline", // The name of a network operator
	"isRooted": 0, // Rooted (jailbroken) device (1 - rooted)
	"userAgent": "a lot of info" // Browser user-agent
}]
```

```json
"pl": [{
	"data": {
		"age": 21, //Reserved. User's age in years
		"cheater": true, //Reserved. True, if a user is a cheater 
		//In case you have your own methods to detect
		//cheaters, you can mark such users.
		//Event records made by cheaters will be 
		//ignored when counting statistical metrics. 

		"tester": true, //Reserved. True, if a user is a tester 
		//Attention! This marker cannot be removed
		//through the SDK (It can not be set to false
		//after true).
		//Event records made by testers will be 
		//ignored when counting statistical metrics. 

		"gender": 1, //Reserved. User's sex 0-unknown, 1-male, 2-female 
		"name": "John Doe", //Reserved. User's name
		"email": "john@email.com", //Reserved. User's e-mail 
		"phone": "+15555555555", //Reserved. User's phone number 
		"photo": "http://google.com/pic.png", //Reserved. User's photo

		//Custom characteristics of a user in a key-value format 
		"key1": "stringValue", //String value
		"key2": 1.54, //Number value
		"key3": [1, 2, "something"] //Array
	},
	"timestamp": 12313 //The date of data changes
}]

"pl": [{
	"data": {
		"key1": null, //Remove user's characteristic key1
		"key2": null //Remove user's characteristic key2
	},
	"timestamp": 12313
}]
```

### Information about an app

The recommended interval of sending is not more than once per 24h.

```json
"ai": [{
	"timestamp": 1386259227,
	"sdkVersion": "1.1", // Required. In case of communication via API, 
	// the version of implementation of a communication mechanism
	"appVersion": "1.2", // App version
	"codeVersion": 14.0, // Version of app's code 
	"bundleId": "com.myown.app" // App's bundle 
}]
```

### Information about a device

The recommended interval of sending is not more than once per 24h.\
Information is the most relevant for mobile devices.

```json
"di" :[
    {
    	  "timestamp" : 1386259227,
        "manufacturer" : "Apple",                   // The manufacturer of a device
        "model" : "iPhone 4,1",                     // uname (iOS), MODEL (Android)
        "screenResolution" : "1024x768",            // The display's resolution of a device
        "screenDpi" : 144,                          // The density of display's points
        "odin" : "klflaiuewfuasydfiasydpf98ay4",    // DeviceUniqueId SHA-1 (Windows Phone),
                                                    // AndroidId SHA-1 (Android)
        "openUdid" : "f;isyofa7w8yp4fapw49",        // OpenUDID ((Android, iOS, Windows phone)
        "idfa" : "AYTSD-ADSYS-LAUSDY-IUAYSD",       // Ad identifier IDFA (iOS)
        "idfv" : "87ASD-9A7SD-AD2G-Q26EO-AS7D",     // Device identifier within the vendor IDFV (iOS)
        "d2dUdid" : "dsufyoa-sfa3wr-ra3rawQ2-AWR3A",// Base64 от DeviceUniqueId (Windows Phone) , 
                                                    // random by http:// www.ietf.org/rfc/rfc4122.txt (Android)
        "imei" : "w87ea6owe7aow78eaow",             // IMEI (Android)
        "androidId" : "o8a7d6oa8s7b6doa87sb6d8bs",  // AndroidID (Android)
        "advertisingId" : "38400000-8cf0-11bd-b23e-10b96e40000d", // Advertising ID (Android, Windows phone 8.1)
        "serialId" : "asd76asd9",                   // Hardware serial number  (Android,Windows phone)
        "deviceVersion": "8.1",                     // OS version
        "deviceModel": "Windows"                    // The name of OS family
    }
]
```

### Session&#x20;

In case it is possible to fix the length of a session, it is sent during the end of a session or during the next session. If there is no such possibility, send the date of the start of a session, the length of a session must be placed in the middle in case it is known. The absence of a parameter of a session's length makes it impossible to count some metrics. &#x20;

```json
"gs" : [
    {
        "timestamp" : 1386259227,   // The date of session's start 
        "length" : 34,              // The length of a session in seconds
        "level" : 3,                // Player's level 
        "inProgress" : ["village"]
    },
    …
]
```

### Real Payment

In case you transfer data related to several transactions, group them by item name.

```json
"rp": [
    {
        "name": "inapp_name_1",         // The name of a purchase 
        "entries": [
            {
                "orderId": "124567.7654321",// Transaction identifier (max. 64 symbols)
                "price": 1.99,              // The price of a purchase in a transaction currency
                "currencyCode": "USD",      // Transaction currency(ISO 4217 format)
                "timestamp": 1386259227,    // The date of a transaction
                "level": 3,                 // Player's level
                "inProgress": ["village"]   // Location (game level) in which an action has been performed
            },{     //If there is one more identical purchase for a report period
                "orderId": "1224567.7634327",
                "price": 1.99,
                "currencyCode": "USD",
                "timestamp": 1386259227,
                "level": 3,
                "inProgress": ["village"]
            }
        ]
    },{     //If there are other purchases for a report period 
        "name": "inapp_name_2",
        "entries": [
            {
                "orderId": "1234567.7654321",
                "price": 9.99,
                "currencyCode": "USD",
                "timestamp": 1386259227,
                "level": 3,
                "inProgress": ["town"]
            }
        ]
    }
]
```

### Onboarding (tutorial) steps

The event allows to learn how a user goes through a tutorial. The event is created after every completed step.

{% hint style="warning" %}
If a tutorial that hasn't been completed is programmed to reset to the beginning, the repeated reference of identical steps will be counted and influence the statistical metrics. &#x20;
{% endhint %}

```json
"tr" : [
    {
        "step" : 1,                // the number of a tutorial step that has been completed 
        "timestamp" : 1386259227,  // date
        "level" : 3,               // player's level
        "inProgress" : ["village"],// location (game level) in which an action has been performed
        …
    }
]
```

Use the following constants for basic actions:

```
"step" : -1  // The beginning of a tutorial(before completing the first step)
"step" : 0   // The tutorial is skipped 
"step" : -2  // The tutorial is completed (instead of the last step's number)
```

In all other cases use the number of steps above 0.&#x20;

### Custom event

If it is necessary to count events that are absent from the main types of events, use user events. An event must have a unique name and can include up to 20 parameters. You can use up to 300 unique names.

{% hint style="warning" %}
We strongly recommend that you do not use custom event properties to transfer and store data that fits the definition of [personal data](https://gdpr-info.eu/issues/personal-data/)!
{% endhint %}

```json
"ce" : [
    {
        "name" : "house_purchase",          // The name of an event (max. 72 symbols)
        "entries" : [
            {
                "t1" : 1231872631,          // The date of an event
                "level" : 3,                // Player's level
                "inProgress" : ["village"], // Location (game level) in which an action has been performed
                "p" : {
                    "t1" : {
                        // up to 10 parameters grouped by data types
                        "double" : {        // Parameters with values - in numbers
                            "key1" : 1,     // The name of a parameter as a key(max. 32 symbols) and
                                            // the value of a parameter
                            "key2" : 2.123,
                            …
                        },
                        "string" : {        // Parameters with values - in strings
                            "key3" : "a",   //  The name of a parameter as a key(max. 32 symbols) and
                                            // the value of a parameter (max. 255 symbols)
                            "key4" : "abc",
                            …
                        }
                    }                       
                }
            },
            {                               // If there are several events with the same name 
            "t1" : 1231872638,
            "level" : 3,                    
            "inProgress" : ["village"],
            "p" : {
                "t1" : {
                    "double" : {
                        "key1" : 1,
                        "key2" : 2.123,
                        …
                    },
                    "string" : {
                        "key3" : "a", 
                        "key4" : "abc",
                        …
                    }
                }                       
            }
        }
    },
    {                                       // If one user had several events with different names for
                                            // a reporting period, continue 
        …
    }
]
```

### New level

This event is for games only.

Allows you to analyze players' distribution by game levels. The event is created when a player moves to the next level. In order to track the average state of game currency accounts, the amount of spendings and earnings of a currency, the amount of a game currency bought while completing a level, you can also send this data in this event.

```json
"lu" : [
    {
        "level" : 10,              // Required. The level that a player got 
        "timestamp" : 1386259227,  // Required. The date of moving to the next level 
        "inProgress" : ["village"],// Location (game level)in which an action has been performed
        "balance" : {              // Optional. The balances of a game currency at the end of a level 
            "money1" : 123,        // The name of a game currency as a key and its amount 
            "money2" : 11,
            …
        },
        "spent" : {                // Optional. The amount of a currency spent on a level
            "money1" : 12,         // The name of a game currency as a key and its amount 
            "money2" : 2,
            "wood" : 12,
            …
        },
        "earned" : {               // Optional. The in-game currency that was earned on a level
            "money1" : 8,    
            "money2" : 2,
            "stone" : 1,
            …
        },
        "bought" : {                // Optional. The in-game currency that was bought on a level 
            "money1" : 10,
            "money2" : 2,
            …
        }
    },
    …
]
```

### Virtual Currency Payment

This event is for games only.

It is used to track the ways in which an in-game currency is spent and the popularity of in-game items.

```json
"ip" :  [
    {
        "purchaseType" : "Weapon",  // The group of an item (max. 96 symbols)
        "purchaseId" : "Dagger",    // The unique name or ID of an item (max. 32 symbols)
        "purchaseAmount" : 1,       // The amount of items bought
        "purchasePrice" : 1.0,      // The price of an item (the overall price of a purchase if there 
                                    // are several identical items are bought) in an in-game currency 
        "purchasePriceCurrency" : "Coins",  // The name of an in-game currency that was used to buy
                                            // an item (max. 24 symbols)
        "timestamp" : 1231872631,   // The date of a purchase
        "level" : 3,                // Player's level
        "inProgress" : ["village"]  // Location (game level) in which an action has been performed
    },
    …
]
```

In case one item is sold in several currencies, a purchase is divided into several events, the amount of which corresponds to the number of currencies. Wherein the number of items is specified only in one event.&#x20;

```json
"ip" :  [
    {
        "purchaseType" : "Weapon",  // The group of an item (max. 96 symbols)
        "purchaseId" : "Dagger",    // The unique name or ID of an item (max. 32 symbols)
        "purchaseAmount" : 1,       // The amount of items bought
        "purchasePrice" : 1.0,      // The price of an item (the overall price of a purchase if there 
                                    // are several identical items are bought) in an in-game currency 
        "purchasePriceCurrency" : "Coins",  // The name of an in-game currency that was used to buy
                                            // an item (max. 24 symbols)
        "timestamp" : 1231872631,   // The date of a purchase
        "level" : 3,                // Player's level
        "inProgress" : ["village"]  // Location (game level) in which an action has been performed
    },
    {
        "purchaseType" : "Weapon", 
        "purchaseId" : "Dagger",   
        "purchaseAmount" : 0,      
        "purchasePrice" : 2.0,     
        "purchasePriceCurrency" : "Gold", 
        "timestamp" : 1231872631,
        "level" : 3,               
        "inProgress" : ["village"]
    },
    …
]
```

### Progression event

This event is for games only.

First of all, the event is used for games with short locations (game levels) that are completed during one game session. The event allows to gather data about the success in location completion and receive statistics by parameters that are changeable during location completion.&#x20;

```json
"pe" : [
    {
        "id" : "location2",         // The name of a location
        "level" : 3,                // Player's level at the moment of location completion 
        "params" : {                // Event parameters
            "source" : "location1", // The name of the previous location of a player 
            "difficulty" : 1,       // Optional. The level of difficulty of location completion 
            "success" : true,       // Success in location completion 
            "duration" : 180        // Optional. Time in seconds of location completion
        },
        "spent" : {                 // Optional. Resources spent during location completion
            "money1" : 12,          // The record of a resource in the format of 
                                    // resource's name - the amount of a resource
            "money2" : 2, 
            "wood" : 12
        }, 
         "earned" : {               // Optional. Resources that are received during location completion 
             "money1" : 8, 
             "money2" : 2, 
             "stone" : 1
        },
        "timestamp" : 1234567890    // The time of exit from a location
    }
]
```

### Ad impression

The event is used for individual tracking of ad revenue.

{% hint style="info" %}
Do not use this event if you use ad networks that utilize the server-server protocol for sending ad revenue data (ironSource, AppLovin MAX, and Fyber networks) and you already set up this method of data collection because if you use both data sources, your revenue data may be duplicated.
{% endhint %}

Example:

```json
"adrv": [{
	"ad_network": "TestAdNetwork", //Name of the ad network responsible for the impression (from 1 to 100 symbols)
	"revenue": 0.3434, //Reward for banner display in USD
	"ad_unit": "TestAdUnit", //Banner name (from 1 to 100 symbols,optional)
	"placement": "TestPlacement" //Banner placement (from 1 to 100 symbols, optional)
}]
```

### Connection to social networks

Allows you to track existing connections to social networks.

```json
"sc" : [
    {
        "socialNetwork" : "FB",   // The name of a social network. The value from the list
                                  // of constants for popular networks or your own string name
        "timestamp" : 1386259227, // The date of connection
        "level" : 3,              // Player's level
        "inProgress" : ["village"]// Location (game level) in which an action has been performed
    },
    ...
]
```

Constants that are supported by the system:

| EN | Evernote      | RT | Reddit   |
| -- | ------------- | -- | -------- |
| FB | Facebook      | RR | Renren   |
| GM | Google Mail   | TB | Tumblr   |
| GP | Google+       | TW | Twitter  |
| IN | LinkedIn      | VK | VK       |
| OK | Odnoklassniki | VB | Viber    |
| PI | Pinterest     | WP | WhatsApp |
| QQ | Qzone         |    |          |

### Publication in social networks

Allows you to track publications in social networks. It also allows you to analyze viral channels to optimize marketing efficiency.&#x20;

It is sent after a publication has been approved by a social network.

```json
"sp" : [
    {
        "socialNetwork" : "FB",   // The name of a social network. The value from the list of 
                                  // constants for popular networks or your own string name
        "postReason" : "levelup", // The reason of publication (max. 32 symbols). We recommend you
                                  // to group reasons instead of sending such names as "Level 99 reached".
        "timestamp" : 1386259227, // The date of publication 
        "level" : 3,              // Player's level 
        "inProgress" : ["village"]// Location (game level)in which an action has been performed
    },
    ...
]
```

We recommend to specify actions that encourage to make a publication as a reason:

| <p>For example:</p><ul><li>Start playing</li><li>New level reached</li><li>New building</li><li>New ability</li><li>Quest completed</li><li>New item</li><li>Collection completed</li><li>Invitation</li></ul> | <ul><li>Asking for help</li><li>New Record</li><li>Achievement</li><li>URL sharing</li><li>Recommendation</li><li>Review</li></ul><p>and so on...</p> |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |

### Clearing user data

The "Wipe" function can help when you test your app and/or when you need to clear user data as a part of the gameplay.

By default, when you send an event without any parameters, a user-specified will be "forgotten". We will keep user data in the database, but you won’t be able to find them by their main identifier. Respectively, the following event batch with the same identifier will result in creating a new user. Also, in certain circumstances, you can’t just ‘forget’ some of the users. Therefore, we have provided several flags that might be helpful.

```json
"wipe" : [
    {
        "saveRegistration": true,     // Optional. A new user will inherit registration dates after
                                      // an old user; a new user will not be registered by devtodev as
                                      // the new one. It might be useful when you test your app.
        "savePayingStatus": true,     // Optional. Old user payment data will be copied into a new
                                      // user card (number of payments, amounts and payment dates).
        "saveCheaterTester": true,    // Optional. cheater and tester labels will be copied into
                                      // a new user card. It might be useful when you test your app.
        "saveCustomProperties": true, // Optional. Old user custom fields and values from those fields
                                      // will be copied into the new user card.
        "timestamp" : 1234567890      // Optional. The time of exit from a location
    }
]
```

## Tracking state (GDPR)

### Limiting the processing of user data. The right to erasure.

This event is implemented in accordance with the GDPR requirements.

A developer must use this event in case a user doesn’t want their data to be sent and processed in the devtodev system.

When calling *"ts"* event with the parameter *"isTrackingAllowed": false*, it is a command to the server to delete all user’s personal data that has been collected by devtodev from this app and a command to block the collection of any data of this user in future.

The user will remain listed as an impersonal unit in previously aggregated metrics.

When sending a *true* value, the permission to block data collection is removed.

```json
{
	"JohnDoe": {
		"prev": "LittleJohn",
		"ts" : [{ 
        	"isTrackingAllowed": false,
        	"timestamp" : 1386259227
		}]
	}
}
```

## An example of a package

'POST' <https://api.devtodev.com/stat/v1/?api=ak-npADyEmjxc0usQR52k6it38zUPSloGT7> with gzipped body: &#x20;

```json
{
	"JohnDoe": {
		"prev": "LittleJohn",
		"tr": [{
			"step": 1,
			"timestamp": 1386259227,
			"level": 1,
			"inProgress": ["village"]
		}, {
			"step": 2,
			"timestamp": 1386259236,
			"level": 1,
			"inProgress": ["village"]
		}, {
			"step": 3,
			"timestamp": 1386259288,
			"level": 1,
			"inProgress": ["town"]
		}],
		"gs": [{
			"timestamp": 1386259227,
			"length": 1250,
			"level": 3
		}],
		"pl": [{
			"data": {
				"gender": 1
			}
		}],
		"lu": [{
			"level": 4,
			"inProgress": ["village"],
			"timestamp": 1442392006,
			"balance": {
				"Coins": 1234,
				"Gold": 11
			}
		}],
		"ce": [{
			"name": "Round_finished",
			"entries": [{
				"t1": 1442392451,
				"level": 4,
				"inProgress": ["village"],
				"p": {
					"t1": {
						"double": {
							"Round_time": 83,
							"Score": 2.123
						},
						"string": {
							"Result": "Victory",
							"Type": "Flawless"
						}
					}
				}
			}, {
				"t1": 1442393455,
				"level": 4,
				"inProgress": ["town"],
				"p": {
					"t1": {
						"double": {
							"Round time": 102,
							"Score": 1.5
						},
						"string": {
							"Result": "Defeat",
							"Type": "Shameful"
						}
					}
				}
			}]
		}],
		"pe": [{
			"id": "town",
			"level": 3,
			"params": {
				"source": "vilage",
				"difficulty": 2,
				"success": true,
				"duration": 180
			},
			"spent": {
				"Turns": 54,
				"Boost Bomb": 1,
				"Extra 5 Turns": 1
			},
			"earned": {
				"Stars": 3,
				"Score": 1200,
				"Coins": 5
			},
			"timestamp": 1234567890
		}],
		"ip": [{
			"purchaseType": "Weapon",
			"purchaseId": "Dagger",
			"purchaseAmount": 1,
			"purchasePrice": 30.0,
			"purchasePriceCurrency": "Coins",
			"timestamp": 1442393479,
			"level": 4,
			"inProgress": ["village"]
		}],
		"rp": [{
			"name": "Currency pack 1",
			"entries": [{
				"orderId": "1234567.7654321",
				"inProgress": ["village"],
				"level": 4,
				"price": 1.99,
				"currencyCode": "USD",
				"timestamp": 1442393460
			}]
		}],
		"sp": [{
			"socialNetwork": "FB",
			"postReason": "New level reached",
			"level": 4,
			"inProgress": ["village"],
			"timestamp": 1442392006

		}]
	}
}
```

## Utility for testing&#x20;

[A utility for sending packets](https://www.devtodev.com/upload/files/devtodevapitester.jar)

<https://www.devtodev.com/upload/files/devtodevapitester.jar>

## An example of sending to PHP

An example of the implementation of a request to PHP with the use of Curl. The sending of real payment.&#x20;

```php
$url = 'https://api.devtodev.com/stat/v1/?api='.$api;
$params = [
        $uid=>[
            'rp' =>[
                [
                    'name' => $name,
                    'entries' =>[
                        [
                            'orderId' => $transactionId,
                            'price' => 100,
                            'currencyCode' => 'USD',
                            'timestamp' => time(),
                            'level'=> 5,
                            'inProgress'=> ['village']
                        ]
                    ]
                ]
            ]
        ]
];

$curlHandle = curl_init($url);
curl_setopt($curlHandle, CURLOPT_HTTPHEADER, array('Content-Type: text/plain;charset=UTF-8'));
curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curlHandle, CURLOPT_POST, TRUE);
curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/4.0 (compatible;)");
curl_setopt($curlHandle, CURLOPT_POSTFIELDS, gzencode(json_encode($params)));
curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curlHandle, CURLOPT_ENCODING, 'gzip');
$response = curl_exec($curlHandle);
```


# Import historical data via API

{% hint style="info" %}
Historical data import is available for [Business and Enterprise price plans](https://www.devtodev.com/promo/pricing).
{% endhint %}

{% embed url="<https://youtu.be/x2tKa3K8C6c?feature=shared>" %}
Historical data import overview
{% endembed %}

If you want to migrate to devtodev from another analytical system, move your data from your company's data collection mechanism, or use the data stored on your servers, feel free to use our Import Data Wizard.

We recommend that you start importing your historical data no later than one month after connecting your project to devtodev. At the same time, it is desirable that the right-hand border of the data interval is as close as possible to the start date of importing your historical data to devtodev.

You can import any data about users or events that are allowed by the devtodev data API. The most frequent task is to import an existing user database (with information about registration dates, player character levels, and other characteristics), data on payments, and user sessions.  You can also dispatch other events but it is not reasonable to cover a period longer than 90 days from the import start date.

The main condition for the successful import of historical data is the match of IDs that are currently dispatched for user/device identification from the devtodev SDK built into your project, with the IDs in your possession - the IDs to which you can link the imported historical data to.

The best option of importing historical data is when you set custom user IDs tracking in the devtodev system. A custom user ID in the devtodev system is an ID assigned by the developer (see setUserId method in the [devtodev SDK integration](/integration/integration-of-sdk-v2/sdk-integration) documentation for the corresponding platform). This is usually the number of the record about the user in your database or a third-party ID by which you authorize and identify the user.

{% hint style="warning" %}
Attention! By default, devtodev uses device ID for identification. Switching the project to identification by user ID can be done by contacting your account manager or by writing a request to our technical support. Switching to identification by user ID is irreversible!&#x20;
{% endhint %}

After the date of switching the project to identification by user ID, it is advisable to wait 7 days before the start of importing historical data.&#x20;

{% hint style="warning" %}
But there is a nuance when it comes to importing historical data - during data merging, the data obtained from third parties will be lost (statistics from markets, data on traffic sources from advertising trackers, and data on income received from advertising networks). If you have such data, then after completing the import process, contact your account manager and they will try to reload the data for the required period.
{% endhint %}

To start the process of importing historical data, go to the settings of the project into which you want to load historical data and select Import Historical Data.

### **The process of loading historical data consists of several stages:**&#x20;

1. **Preparation stage**\
   Click on the start button on the Historical data page. A temporary project will be created in devtodev (you can see it in the list of projects). It will have the same name as the original project, but with the addition of the TMP suffix. You will need to export your historical data to this temporary project. To upload, use the API key that you see on the Import Historical data page. Check out the [devtodev data API documentation](/integration/server-api/data-api-2.0). Prepare a script that will send the historical data of the project to devtodev. It is extremely important that events are dispatched in chronological order for (at least) each user individually (in a JSON file the dispatched events should be ordered starting from the older ones at the beginning of the file to the newer ones at the end).&#x20;
2. **Data loading stage**\
   After you have prepared the data for loading and are ready to start exporting them to devtodev, click the Start loading data button. At this moment, our server will switch to the mode of receiving historical data. Load the prepared data. If there is a lot of data, then you can expect the loading process to take up to several days. You should aim to keep it within 2 weeks.
3. **Processing uploaded data**\
   After your script has finished uploading data to devtodev, click the Upload Finished button on the data upload step. After clicking this button, we will start transferring the uploaded data to our database and calculating metrics for this period of time. The calculation can take up to several hours. At the end of the data processing, the interface for loading historical data will automatically proceed to the next step - data verification. Once data processing is completed we will additionally send you a bell notification.
4. **Reviewing the loaded data**\
   This is an extremely important step in the data loading process because it is here that you understand whether you did everything right and are satisfied with the result, or something went wrong, which means that you have to implement the necessary changes and try importing again. \
   \
   Open the devtodev interface and go through all the temporary project reports that can be built from the data you have imported. It is best if you compare the metric data aggregated by devtodev after importing with the metric data aggregated by the analytical system from which you are migrating. \
   \
   If you see incorrect data in the reports (the data does not match the information from your previous analytical system reports), try to find out what could have caused this problem, and to be more accurate, what data could be loaded incorrectly. Contact devtodev support if you are unable to determine the source of the problem. \
   To reupload historical data, click the Clear uploaded data button. Then the data will be deleted and you can try again. If you don’t want to make another attempt, click the Cancel process button. \
   \
   If devtodev shows the data you expected to see - hooray! You have succeeded and you can complete the migration proces&#x73;**.**

{% hint style="warning" %}
Attention! If you agree with the result and complete the process of importing historical data (click the Verified button), then re-export or adding another chunk of historical data will be impossible. This action is irreversible!
{% endhint %}

5\. **Historical data is loaded**

Well done, not everyone can reach this stage! Your temporary project ceased to exist. From now on, only the project with the loaded historical data is available to you.

### **Learn more about the specifics of loading historical data using API**

You can encapsulate the events either by using historical streamflow or by sending all events for each user individually. But the main thing is that events must be ordered by the date from the oldest to the newest in both each individual parcel and during the entire data loading process.

As first events, we recommend sending data about the user/device and the application, dating them with the date of user registration. Then you can send any other events.

This is an example of sent data:&#x20;

```json
{
    "reports": [
        {
            "deviceId": "user id",
            "userId": "user id",
            "packages": [
                {
                    "language": "en",
                    "country": "GB",
                    "appVersion": "1.2",
                    "events": [
                        {
                            "code": "di",
                            "osVersion": "10.2.2",
                            "os": "iOS",
                            "displayPpi": 401,
                            "displayResolution": "1920x1080",
                            "dispalyDiagonal": "5.5",
                            "manufacturer": "Apple",
                            "model": "iPhone8,2",
                            "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/602.3.12 (KHTML, like Gecko) Version/10.0.2 Safari/602.3.12",
                            "timeZoneOffset": 7200,
                            "idfv": "30FE1CE1-1125-4657-97B0-638744C3C6D1",
                            "idfa": "0A60DCF2-3186-4801-9192-D8CFA995DD6D",
                            "timestamp": 1710772505122
                        },
                        {
                            "code": "ss",
                            "timestamp": 1710772505123,
                            "level": 1
                        },
                        {
                            "code": "pl",
                            "timestamp": 1710772511089,
                            "sessionId": 1710772505123,
                            "level": 1,
                            "parameters": {
                                "nickname": "John Doe",
                                "cheater": false
                            }
                        },
                        {
                            "code": "tr",
                            "timestamp": 1710772715371,
                            "sessionId": 1710772505123,
                            "level": 1,
                            "step": -1
                        },
                        {
                            "code": "tr",
                            "timestamp": 1710772725344,
                            "sessionId": 1710772505123,
                            "level": 1,
                            "step": 1
                        },
                        {
                            "code": "lu",
                            "timestamp": 1710772736345,
                            "sessionId": 1710772505123,
                            "level": 2,
                            "balance": {
                                "money1": 123,
                                "money2": 11
                            }
                        },
                        {
                            "code": "tr",
                            "timestamp": 1710772741456,
                            "sessionId": 1710772505123,
                            "level": 2,
                            "step": 2
                        },
                        {
                            "code": "tr",
                            "timestamp": 1710772752425,
                            "sessionId": 1710772505123,
                            "level": 2,
                            "step": -2
                        },
                        {
                            "code": "ce",
                            "timestamp": 1710772773675,
                            "sessionId": 1710772505123,
                            "level": 2,
                            "name": "eventName",
                            "parameters": {
                                "intParameter": 134,
                                "stringParameter": "hello",
                                "doubleParameter": 12.98
                            }
                        },
                        {
                            "code": "rp",
                            "timestamp": 1710772798278,
                            "sessionId": 1710772505123,
                            "level": 2,
                            "productId": "com.example.application.starterpack",
                            "orderId": "280001601071201",
                            "price": 19.99,
                            "currencyCode": "USD"
                        },
                        {
                            "code": "ue",
                            "timestamp": 1710772898278,
                            "level": 2,
                            "length": 393,
                            "sessionId": 1710772505123
                        }
                    ]
                }
            ]
        }
    ]
}
```


# Data Export

Here you will find how you can export your data from devtodev.

{% hint style="info" %}
Raw data export is available for [Basic, Business and Enterprise price plans](https://www.devtodev.com/promo/pricing).
{% endhint %}

## Export via API

Use devtodev API to export raw data.&#x20;

{% content-ref url="/pages/-LyhAC0Ly5EcTxM-jiNZ" %}
[Raw Export](/integration/server-api/raw-export)
{% endcontent-ref %}

## Raw Data Export via devtodev interface

Build a report and download it as a .csv file right in the devtodev interface.

{% content-ref url="/pages/-M-Q4nDv8ChS7Jqn5Ok5" %}
[Tuning](/reports-and-functionality/project-related-reports-and-fuctionality/tuning)
{% endcontent-ref %}

## Export to Cloud Storage

Configure data export to a data storage.

{% content-ref url="/pages/-Mb\_J9B648-LC\_NyzOH5" %}
[Data Export to Cloud Storage (BigQuery / Amazon S3)](/integration/data-export/data-export-to-cloud-storage-bigquery-amazon-s3)
{% endcontent-ref %}


# Data Export to Cloud Storage (BigQuery / Amazon S3)

{% hint style="info" %}
Cloud export is available for [Business and Enterprise price plans](https://www.devtodev.com/promo/pricing).
{% endhint %}

devtodev has an option to export user and event data to a cloud storage. The event data is uploaded once every hour. User data is uploaded every day if we receive at least one event for the last 24 hours.

To export your data to one of the supported cloud storages, please send a request to <info@devtodev.com>.

## Export to BigQuery

To export data to BigQuery you will need to:

1. Create a service account, if it does not already exist.
2. Get service account credentials.
3. Create a dataset.
4. Choose what kind of data you want to export to your dataset.

In the request specify the following details:&#x20;

1. Service account credentials;
2. Name and location of the dataset in BigQuery;&#x20;
3. Export configuration ([see below](#export-configuration)).

### Creating a service account in BigQuery

1. If you do not have a service account, create one by following the [Google Cloud manual](https://cloud.google.com/iam/docs/creating-managing-service-accounts#creating).
2. Your service account has to have rights for table creation and data upload. Add **bigquery.user** or **bigquery.admin** role to your service account. \
   Make sure to add these permissions to your role:
   * bigquery.jobs.create
   * bigquery.tables.create
3. Create credentials for your service account, if there are none yet. Follow [this manual](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) to create access keys. To create keys, add **serviceAccountKeyAdmin** role to your service account.

### Creating a dataset

Follow [this manual](https://cloud.google.com/bigquery/docs/datasets) to create a dataset in BigQuery.

Name your dataset **devtodev**, that way we can send your data to BigQuery.&#x20;

Also, while creating a dataset, keep **location** in mind.&#x20;

{% hint style="danger" %}
**You cannot change the location of the dataset later!** [More on locations in BigQuery](https://cloud.google.com/bigquery/docs/locations).
{% endhint %}

### Export configuration

After creating a service account and a dataset we need to configure export in devtodev.

You can choose one of two ways to export your data:

**Export data to one table** — all event data will be uploaded to one common table named **p\<project id>\_events**. &#x20;

**Export data by event type** — every event type will be uploaded to their respective table. *The list of event types is below.*&#x20;

Every event type will have a table with a name like this **p\<project id>\_events\_\<event type>\[**\_\<event name>**]**. For example:

* **p234\_rp** —this is a table for **real payment** events from a project with id 234.
* **p234\_ce\_mission\_start** — this is a table for a **custom event** named “mission\_start“  from a project with id 234.

You can match project name and project id in the **\_projects** table, which will be automatically filled at the time of the first export.

Active user information will be uploaded to a separate table named **p\<project id>\_users** regardless of how you choose to upload event data.

## Export to Amazon S3

To export data to Amazon S3 you will need to:

1. Create an account, if it does not already exist.
2. Get credentials (accessKey and secretKey).
3. Create a bucket.
4. Choose what kind of data you want to export to your bucket.

In the request specify the following details:&#x20;

1. Account credentials;
2. Name and region of the bucket in Amazon S3;&#x20;
3. Export configuration ([see below](#export-configuration-1)).

### Creating an Amazon S3 account

If you do not already have an account, follow [this AWS manual](https://docs.aws.amazon.com/AmazonS3/latest/userguide/setting-up-s3.html) to create one.

### Getting credentials

See [this manual](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-where) for more detail on how to find your credentials.

We need **accessKey** and **secretKey** which are located in \~/.aws/credentials file. We will also need your **region** information, it is located in \~/.aws/config file. Execute [aws configure](https://docs.aws.amazon.com/cli/latest/reference/configure/index.html) command in AWS developer console to get **accessKey** and **secretKey**.

Example:

```
$ aws configure
AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE
AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Default region name [None]: us-west-2
Default output format [None]: json
```

### **Creating a bucket**

Follow [this manual](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-overview.html) to create a bucket in S3.

The Name of the bucket should be unique, see [more on bucket naming](https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html). Also, while creating a dataset, keep the **region** in mind.&#x20;

{% hint style="warning" %}
Objects can never leave the region unless they are explicitly transferred! [More on AWS regions](https://docs.aws.amazon.com/general/latest/gr/rande.html).
{% endhint %}

### Export configuration

After creating an account and a bucket we need to configure export in devtodev.

Your data will be stored in a bucket directory named **p\<project id>** which will store .csv files compressed with gzip. Each directory will have a **project\_info.txt** file with the **project name** and **application id** in devtodev service.

You can choose one of two ways to export your data:

**Export data to one table** — all event data will be uploaded to one **common** table.&#x20;

Example of such table: 2021\_05\_26\_08\_00\_54**common**86ddf8a5-1e7f-4f2c-a4d3-22f4d6a8860c

**Export data by event type** — every event type will be uploaded to their respective table. *The list of event types is below.*

Some examples:

* 2021\_05\_26\_08\_08\_28**ce**\[editor\_item\_remove]a9413576-0a32-4a2a-ad84-940150e9a218 — this is a table for a **custom event** named “editor\_item\_remove“.&#x20;
* 2021\_05\_26\_08\_08\_11**rp**556dbd8d-71c9-41b4-9564-d43b39ca1b7d — this is a table for **real payment** events.

Active user information will be uploaded to a separate users table regardless of how you choose to upload event data.&#x20;

Example of such table: 2021\_05\_26\_08\_07\_52**users**439c129f-d70b-4f98-ad86-4cb01054732b

## **List of event types**

For export configuration, you can select the type of events you want to export. You can also select which project to export.&#x20;

The list below contains event types (with fields) available for export.

### Common basic fields for all event types

```
devtodev_id — numeric user id
main_id — string user id 
uc_platform_key — platform identifier for cross-platform projects
crossplatform_id — custom user id, only for projects with identification by user id 
uc_createtime — user registration date
uc_first_paymenttime — first payment date
uc_last_paymenttime — last payment date
uc_payment_cnt — number of payments
uc_payment_sum — sum of payments
uc_level — user level
uc_country — country
uc_language — language
uc_cheater — mark a cheater
uc_tester — mark a tester
```

### Event types

<table><thead><tr><th width="257.9296875">Event name</th><th width="125.93098958333331">Event code</th><th>Additional fields</th></tr></thead><tbody><tr><td>EventTrackingStatus</td><td>ts</td><td>allow_tracking — is tracking allowed</td></tr><tr><td>EventUserInfo</td><td>ui</td><td><p>language — device locale</p><p>custom_udid — custom user id</p></td></tr><tr><td>EventDeviceInfo</td><td>di</td><td><p>device_version</p><p>device_os</p><p>display_resolution</p><p>display_dpi</p><p>androidid</p><p>idfa</p><p>idfv</p><p>advertisingid</p><p>serialid</p><p>manufacturer</p><p>model</p><p>device_model</p><p>offset — user timezone offset<br></p></td></tr><tr><td>EventDeviceInfoV2</td><td>di</td><td><p>device_version</p><p>device_os</p><p>display_resolution</p><p>display_dpi</p><p>display_diagonal</p><p>manufacturer</p><p>model</p><p>offset — user timezone offset</p><p></p><p>androidid</p><p>openudid

</p><p>idfa</p><p>idfv

</p><p>advertisingid</p><p>serialid</p><p>install_source</p><p>user_agent</p><p></p></td></tr><tr><td>EventRealPaymentEntry</td><td>rp</td><td><p>currency</p><p>product</p><p>payment_id</p><p>price_usd</p><p>payment_status</p></td></tr><tr><td>EventGamePurchase</td><td>ip</td><td><p>amount</p><p>item_type</p><p>item</p><p>inapp_currencies — structure with info on currency type and its amount spent on item purchase</p></td></tr><tr><td>EventCustomColumnar</td><td>ce</td><td><p>event_name</p><p>event_params — structure with parameter names and values</p></td></tr><tr><td>EventProgression</td><td>pe</td><td><p>location</p><p>spent</p><p>earned</p><p>source</p><p>difficulty</p><p>success</p><p>duration</p></td></tr><tr><td>EventTester</td><td>tstr</td><td>tester</td></tr><tr><td>EventCheater</td><td>ch</td><td>cheater</td></tr><tr><td>EventRegistrations</td><td>rg</td><td>this event only has basic fields</td></tr><tr><td>EventGameSessionStart</td><td>ss</td><td>amount</td></tr><tr><td>EventUserEngagement</td><td>ue</td><td>duration</td></tr><tr><td>EventPeople</td><td>pl</td><td><p></p><p>event_params — custom user property fields</p></td></tr><tr><td>EventSocialNetworkPost</td><td>sp</td><td>network<br>reason</td></tr><tr><td>EventSocialNetworkConnect</td><td>sc</td><td>network</td></tr><tr><td>EventTutorial</td><td>tr</td><td><p></p><p>step</p></td></tr><tr><td>EventLevelUp</td><td>lu</td><td><p>local_duration</p><p>absolut_duration</p><p>spent</p><p>earned</p><p>balance</p><p>bought</p><p></p></td></tr><tr><td>EventApplicationInfo</td><td>ai</td><td><p>sdk_version</p><p>app_version</p><p>bundle_id</p><p>engine</p><p></p><p></p></td></tr><tr><td>EventWipe</td><td>wipe</td><td><p>save_cheater_tester</p><p>save_custom_props</p><p>save_paying_status</p><p>save_registration</p></td></tr><tr><td>EventAlive</td><td>al</td><td>this event only has basic fields</td></tr><tr><td>EventReferal</td><td>rf</td><td><p>publisher</p><p>sub_publisher</p><p>sub_ad</p><p>sub_ad_group</p><p>sub_campaign</p><p>sub_placement</p><p>sub_site</p><p>cost</p><p></p></td></tr><tr><td>EventSubscription</td><td>sbs</td><td><p>source<br>payment_type<br>start_time<br>expiry_time<br>event_type<br>price_usd<br>product<br>original_payment_id </p><p>payment_id<br>purchase_type<br>promo_code<br>promo_type<br>payment_status<br>eventlevel</p></td></tr><tr><td>EventAdRevenue</td><td>adrv</td><td><p>source<br>ad_unit<br>ad_network </p><p>placement </p><p>revenue</p></td></tr></tbody></table>

### User data&#x20;

User data is uploaded every day if we receive at least one event for the last 24 hours.&#x20;

| Field name             | Decription                                                                                                                                                                                                                                                                                |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| devtodev\_id           | Numeric user id in devtodev project                                                                                                                                                                                                                                                       |
| uc\_user\_id           | Main user identifier (device advertising id by default)                                                                                                                                                                                                                                   |
| uc\_custom\_uid        | Custom user identifier set by developer (crossplatform\_id)                                                                                                                                                                                                                               |
| uc\_platform\_key      | <p><strong>Only for</strong> <a href="/pages/EBjsmNHf2V14SSZuYNZr"><strong>cross-platform projects</strong></a><strong>.</strong> Platform identifier. <br>Stores a separate value for each platform where the user registered + "general" for the General section of the User card. </p> |
| uc\_idfv               |                                                                                                                                                                                                                                                                                           |
| uc\_idfa               |                                                                                                                                                                                                                                                                                           |
| uc\_android\_id        |                                                                                                                                                                                                                                                                                           |
| uc\_advertising\_id    |                                                                                                                                                                                                                                                                                           |
| uc\_offset             | User timezone offset                                                                                                                                                                                                                                                                      |
| uc\_publisher          |                                                                                                                                                                                                                                                                                           |
| uc\_device             | Device model                                                                                                                                                                                                                                                                              |
| uc\_createtime         | User registration date (first app launch)                                                                                                                                                                                                                                                 |
| uc\_lastseen           |                                                                                                                                                                                                                                                                                           |
| uc\_first\_paymenttime |                                                                                                                                                                                                                                                                                           |
| uc\_last\_paymenttime  |                                                                                                                                                                                                                                                                                           |
| uc\_payment\_sum       | Payments sum                                                                                                                                                                                                                                                                              |
| uc\_payment\_cnt       | Number of payments                                                                                                                                                                                                                                                                        |
| uc\_level              | User level stored in the User Cards                                                                                                                                                                                                                                                       |
| uc\_app\_version       | Current app version                                                                                                                                                                                                                                                                       |
| uc\_sdk\_version       | Current SDK version                                                                                                                                                                                                                                                                       |
| uc\_os\_version        | Current OS version                                                                                                                                                                                                                                                                        |
| uc\_country            |                                                                                                                                                                                                                                                                                           |
| uc\_language           |                                                                                                                                                                                                                                                                                           |
| uc\_cheater            | Cheater mark                                                                                                                                                                                                                                                                              |
| uc\_tester             | Tester mark                                                                                                                                                                                                                                                                               |
| uc\_custom\_props      | List of Custom User Properties                                                                                                                                                                                                                                                            |
| uc\_sub\_publisher     |                                                                                                                                                                                                                                                                                           |
| uc\_sub\_campaign      |                                                                                                                                                                                                                                                                                           |
| uc\_sub\_keyword       |                                                                                                                                                                                                                                                                                           |
| uc\_sub\_placement     |                                                                                                                                                                                                                                                                                           |
| uc\_sub\_site          |                                                                                                                                                                                                                                                                                           |
| uc\_sub\_ad\_group     |                                                                                                                                                                                                                                                                                           |
| uc\_sub\_ad            |                                                                                                                                                                                                                                                                                           |
| ad\_tracker\_id        | Additional identifier for aсquisition                                                                                                                                                                                                                                                     |


# Integration of SDK 1.0+ (deprecated)


# SDK Integration


# iOS

{% hint style="danger" %}
**This generation of SDK is deprecated and is no longer supported.**\
Information about the [current version can be found here](/integration/integration-of-sdk-v2/sdk-integration/ios).
{% endhint %}

Please perform the following actions to integrate your application with devtodev system:

* add the application to the Space using the wizard for adding application
* [download the latest version of devtodev SDK](https://github.com/devtodev-analytics/ios-sdk) or install via CocoaPods
* integrate SDK into your application. The integration may be whether partial or including all the possibilities.

## CocoaPods

[CocoaPods](http://cocoapods.org/) is the easiest way to add devtodev into your iOS project.

1. Firstly, install CocoaPods using

   ```ruby
   gem install cocoapods
   ```
2. Create a file in your Xcode project called Podfile and add the following:

   ```ruby
   pod 'devtodev'
   ```
3. Run

   ```ruby
   pod install
   ```

   in your Xcode project directory. CocoaPods should download and install the devtodev library, and create a new Xcode workspace. Open this workspace in Xcode.

## Manual installation

1. Download the latest version of devtodev SDK from the [GitHub](https://github.com/devtodev-analytics/ios-sdk) repository.
2. Include devtodev.framework dependency:

   ![](/files/-LnlSi943IDSPRSrYBWB)
3. Link against the embedded framework:

   Add devtodev.framework to the Linked Frameworks and Libraries section.

   ![](/files/-LnlSi96uHqGJ6LYqibP)
4. For the correct SDK functioning add the following frameworks:

   * Security.framework (**Optional**) &#x20;
   * UIKit.framework (**Optional**) &#x20;
   * UserNotifications (**Optional**) &#x20;
   * StoreKit.framework &#x20;
   * AdSupport.framework &#x20;

   ![](/files/-LnlSi98BZCoJeeCnP9L)
5. Add init method into didFinishLaunchingWithOptions method of your AppDelegate.m

   ```
   /**
   * devtodev App Id and Secret key can be found in the devtodev application
   * settings page (“My apps” → App Name → “Settings” → “Integration”)
   */
   [DevToDev initWithKey:applicationId andSecretKey:secretKey];
   ```
6. If the application you integrate SDK in is a part of a cross-platform project, then the user data initialization is required.\
   Since the analytics of cross-platform projects is based on an unique user (unlike the usual projects where it is based on device identifiers), you have to:
   * Set the unique cross-platform user identifier (it will be used for cross-platform project data collection).    &#x20;
   * Actualize the user data. Mostly it is about game applications where the player has a game level as a characteristic. For such projects, you need to set the current player level.

We recommend you set the user identifier before SDK initialization, otherwise the user identifier from the previous session will be used since the SDK initialization moment till the setUserID method call.

{% hint style="info" %}
If your cross-platform application is supposed to be used without cross-platform authorization, don't use the setUserID method or use the empty string ("") as the user identifier. SDK will assign the unique identifier to the user. This identifier will be used until the real cross-platform identifier is assigned to the user.
{% endhint %}

```objectivec
/**
* Method allows to initialize the user. It applies when SDK initialization or user relogin.
* @param NSString activeUserId - unique cross-platform user identifier (max. 64 symbols)
*/
[DevToDev setUserId:@"activeUserId"];

/**
* Method sets the current user level. Using this method allows to actualize 
* the SDK user data in game cross-platform applications.
* @param NSUInteger level - number of current game level of the user
*/
[DevToDev setCurrentLevel:level];

/**
* devtodev App Id and Secret key can be found in the devtodev application
* settings page ("Settings" → "SDK" → "Integration")
*/
[DevToDev initWithKey:applicationId andSecretKey:secretKey];
```

{% hint style="warning" %}
If your application allows user to re-login (changing the user during the working session of application), then the *setUserID* and *setCurrentLevel* methods should be called just after the authorization. You don't need to call the SDK initialization one more time.
{% endhint %}

### Debug mode

To enable the debug mode and make SDK notifications displayed in the console use this method:

```objectivec
/**
* @param BOOL isActive
*/
[DevToDev setActiveLog: (BOOL) isActive];
```


# Android

{% hint style="danger" %}
**This generation of SDK is deprecated and is no longer supported.**\
Information about the [current version can be found here](/integration/integration-of-sdk-v2/sdk-integration/android).
{% endhint %}

SDK is available as a library in AAR (recommended) and JAR. The library is available in Maven Central repository and on [GitHub](https://github.com/devtodev-analytics/android-sdk) repository.

**Step 1.** If you use Gradle for the applications build, add mavenCentral() into gradle.build file of your application and specify the following relationship in dependencies block:

```java
dependencies {
    implementation 'com.devtodev:android:1.14.10'
    implementation 'com.android.installreferrer:installreferrer:2.2'
    implementation 'com.google.android.gms:play-services-base:17.6.0'
    implementation 'com.google.firebase:firebase-core:19.0.0'
    implementation 'androidx.preference:preference:1.1.1' //or higher, required for SDK version 1.14.8 and higher
}
```

In case you don't use Gradle, you can [download it here](https://dl.bintray.com/devtodev/maven/com/devtodev/android/1.14.8/android-1.14.8.aar) and add the library into the project.

**Step 2.** Initialize the library in the first Activity method onCreate() in the following way:

```java
public class MyActivity extends Activity {
      @Override
      public void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          // Initialization devtodev SDK
          DevToDev.init(this, APP_ID, SECRET_KEY);
      }
}
```

App ID and Secret key can be found in the application settings: "Settings" → "SDK" → "Integration".

{% hint style="warning" %}
The devtodev analytics library contains an implementation of *FirebaseMessagingService* for working with push notifications.\
If you want to use your own or a third-party push notification service instead of implementing  “[devtodev push notification](/integration/integration-of-sdk/push-notifications)”, then you need to disable the built-in service in the manifest file.

Example:

```java
<service
android:name="com.devtodev.push.logic.DTDFcmMessagingService"
android:enabled="false">
</service>
```

{% endhint %}

If you want to use our SDK to work with push notifications, [see this doc](/integration/integration-of-sdk/push-notifications/android).

**Step 3.** Add the following lines at the bottom of proguard.config

```
-keep class com.devtodev.** { *; }
-dontwarn com.devtodev.**
```

## **Additional initialization**

If the application you integrate SDK in is a part of a cross-platform project, then the user data initialization is required.

Since the analytics of cross-platform projects is based on a unique user (unlike the usual projects where it is based on device identifiers), you have to:

* Set the unique cross-platform user identifier (it will be used for cross-platform project data collection).
* Actualize the user data. Mostly it is about game applications where the player has a game level as a characteristic. For such projects you need to set the current player level.

{% hint style="info" %}
We recommend you set the user identifier before SDK initialization, otherwise, the user identifier from the previous session will be used since the SDK initialization moment till the setUserID method call.

If your cross-platform application is supposed to be used without cross-platform authorization, don't use the setUserID method or use the empty string ("") as the user identifier. SDK will assign the unique identifier to the user. This identifier will be used until the real cross-platform identifier assigns to the user.
{% endhint %}

```java
/**
* Method allows to initialize the user. It applies when SDK initialization or user relogin.
* @param String activeUserId - unique cross-platform user identifier (max. 64 symbols)
*/
DevToDev.setUserId(activeUserId);

/**
* Method sets the current user level. Using this method allows to actualize the SDK user data
* in game cross-platform applications.
* @param int level - number of current game level of the user
*/
DevToDev.setCurrentLevel(currentLevel);

/**
* devtodev SDK initialization
* @param String appId - devtodev App Id
* @param String secretKey - Secret key
* devtodev App Id and Secret key can be found in the devtodev application
* settings page ("Settings" → "SDK" → "Integration")
*/
DevToDev.init(getBaseContext(), appId, secretKey);
```

{% hint style="warning" %}
If your application allows user to re-login (changing the user during the working session of application), then the *setUserID* and *setCurrentLevel* methods should be called just after the authorization. You don't need to call the SDK initialization one more time.
{% endhint %}

## **Debug mode**

To enable the debug mode and make SDK notifications displayed in the console use this method:

```java
/**
* @param logLevel
*/
DevToDev.setLogLevel(LogLevel logLevel);
```


# Windows 8.1 and 10

{% hint style="danger" %}
**This generation of SDK is deprecated and is no longer supported.**\
Information about the [current version can be found here](/integration/integration-of-sdk-v2/sdk-integration/windows).
{% endhint %}

{% hint style="warning" %}
You have to enable Internet (enabled by default in Windows 10) and Location (if needed) in the Capabilities tab of Package.appxmanifest for correct work of the SDK.
{% endhint %}

1. [Download the latest version of devtodev SDK from the GitHub repository.](https://github.com/devtodev-analytics/winstore-sdk)
2. To start working with the SDK, add the DevToDev.winmd and DevToDev.Background.winmd to the project references.
3. Initialize the library at Application Launching event.

```csharp
/**
* <param name="appKey">App ID</param>
* <param name="appSecret">Application secret key</param>
*/
DevToDev.SDK.Initialize(string appKey, string appSecret);
```

App ID and Secret key can be found in the application settings (Open "Settings" → "SDK" → "Integration").

Example:

```csharp
DevToDev.SDK.Initialize("3f2504e0-4f89-11d3-9a0c-0305e82c3301", "a8f5f167f44f4964e6c998dee827110c");
```

If the application you integrate SDK in is a part of cross-platform project, then the user data initialization is required.

Since the analytics of a cross-platform projects is based on a unique user (unlike the usual projects where it is based on device identifiers), you have to:

* Set the unique cross-platform user identifier (it will be used for a cross-platform project data collection).
* Actualize the user data. Mostly it is about game applications where the player has a game level as a characteristic. For such projects you need to set the current player level.

{% hint style="info" %}
We recommend you set the user identifier before SDK initialization, otherwise, the user identifier from the previous session will be used since the SDK initialization moment till the UserID field is set.

If your cross-platform application supposes to be used without cross-platform authorization, don't use the UserID field or use the empty string ("") as the user identifier. SDK will assign the unique identifier to the user. This identifier will be used until the real cross-platform identifier assigns to the user.
{% endhint %}

```csharp
DevToDev.SDK.UserID = "activeUserId"; //cross-platform user identifier (64 symbols max.)

/**
* <param name="appKey">App ID</param>
* <param name="appSecret">Application secret key</param>
*/
DevToDev.SDK.Initialize(string appKey, string appSecret);

/**
* <param name="level">Current level</param>
*/
DevToDev.SDK.SetCurrentLevel(int level);
```

{% hint style="warning" %}
If your application allows user to re-login (changing the user during the working session of application), then the *UserID* field and *SetCurrentLevel* method should be called just after the authorization. You don't need to call the SDK initialization one more time.&#x20;
{% endhint %}

## **Debug mode**

To enable the debug mode and make SDK notifications displayed in the console use this method:

```csharp
//to enable logging
DevToDev.SDK.LogEnabled = true;

//to disable loging
DevToDev.SDK.LogEnabled = false;
```


# Web

{% hint style="danger" %}
**This generation of SDK is deprecated and is no longer supported.**\
Information about the [current version can be found here](/integration/integration-of-sdk-v2/sdk-integration/web).
{% endhint %}

Please do the following to integrate your web application with devtodev:

1. Add the application to the Space using the wizard for adding application.
2. [Integration samples of devtodev SDK for WEB are available on GitHub.](https://github.com/devtodev-analytics/web-sdk)
3. To integrate SDK, add the following line to the tag of your page:

```javascript
<script type="text/javascript" src="https://cdn.devtodev.com/sdk/web/v1/devtodevsdk.js">
</script>
```

## Initialization

In order for SDK for WEB to start working, it is necessary to perform initialization right after the page is loaded and you have a basic user identifier at your disposal.

```javascript
/**
* @param {string} apiKey - devtodev API key, unique API key can be found in the application
* settings ("Settings" → "SDK" → "Integration")
* @param {string} userId - Unique user identifier.
* For example, user’s ID in a social network, or a unique account name used
* for user identification on your server.
* @param {string} previousUserId - Previous unique user identifier. Optional.
* It is used in case of change of the user identifier.
*/

devtodev.init(apiKey, userId, previousUserId);
```

{% hint style="warning" %}
In case User ID is changed after SDK was initiated, the method should be called repeatedly with indication of a new User ID. For example, when user signs into another account in a launched messenger application.
{% endhint %}

{% hint style="info" %}
If a user has no unique identifier, e.g. if it is possible to use your application / site without authorization), but you need to get stats for such users, don't set userId during the initialization or set an empty string ("") or null as value of userId. SDK will assign the unique identifier to the user. This identifier will be used until the real identifier assigns to the user.
{% endhint %}

Unique API key can be found in the application settings: "Settings" → "SDK" → "Integration".

## Cross-platform application Initialization

In cross-platform applications, the additional user identifier can be used. It is a user cross-platform ID which is unique for all of the platforms. And if a cross-platform ID differs from the ID that is main for the platform, you need to set the cross-platform ID. The cross-platform ID combines the user data for a cross-platform project.

We recommend you apply this method before the SDK initialization, otherwise, the user identifier from the previous session will be used since the SDK initialization moment till the setCrossplatformUserId method call.

If it is difficult to do, set a cross-platform ID as soon as it is available in the application after SDK initialization.

```javascript
/**
* Initializes the user with the specified cross-platform identifier
* @param {string} сrossplatformUserId - unique cross-platform user ID used
* for user identification on your server.
*/

devtodev.setCrossplatformUserId(сrossplatformUserId);
```

{% hint style="warning" %}
If your application allows user to re-login (changing the user during the working session of application), then the *setCrossplatformUserId* method should be called just after the authorization. You don't need to call the SDK initialization one more time.
{% endhint %}

## Additional initialization

For the most precise data collection, we strongly recommend specifying some information about the user right after SDK is initiated.

### User data initialization

In the first place, this additional initialization is required for gaming applications where the player has a game level as a characteristic.

```javascript
/**
* Initializes the current user level. Required if level feature used in the app.
* @param {number} currentUserLevel- Ð¡urrent game level of the player.
*/

devtodev.setCurrentLevel(currentUserLevel);
```

### Application data initialization

It is not obligatory, but if you want to have the ability to build reports with regard to the version of your application, use this method **before initialization**.

```javascript
/**
* @param {Object} appData - App data object.
* @param {string} appData.appVersion - Current app version. Required.
* @param {number} appData.codeVersion - Current code version. Optional.
*/

devtodev.setAppData(appData);
```

## Debug mode

To enable the debug mode and make SDK notifications displayed in the console, use this method:

```javascript
/**
* Activates console log
* @param {boolean} status
*/

devtodev.setDebugLog(status);
```


# Unity

{% hint style="danger" %}
**This generation of SDK is deprecated and is no longer supported.** Information about the [current version can be found here](/integration/integration-of-sdk-v2/sdk-integration/unity).
{% endhint %}

## Integration

{% hint style="warning" %}
**Only Unity 5.4 and above is supported.**
{% endhint %}

Please do the following to integrate your application with devtodev:

1. Add the application to the Space using the wizard for adding an application.&#x20;
2. **Attention! If your Unity project can be used for compilations for different platforms**, you need to add the applications in devtodev for each platform. As a result, the statistics will be gained for each platform separately.
3. [Download the latest version of devtodev SDK from the GitHub repository.](https://github.com/devtodev-analytics/unity-sdk)
4. **Attention! If you install SDK versions (1.\*) 2.0, 2.0.1 or 2.0.2, you have to delete them before integrating the latest version.**

   To do that, delete the following files and catalogues:

   **For 1.\***

   * Assets/DevToDev/ (the folder) &#x20;
   * Assets/Plugins/Android/ (files android-suport-v4.jar, AndroidManifest.xml, devtodev.jar, devtodev\_android\_wrapper.jar, google-play-services.jar) &#x20;
   * Assets/Plugins/iOS/ (files AccrualType.h, CustomEventParams.h, all DevToDev\*.h, Gender.h, libdevtodev.a, ReceiptStatus.h, SocialNetwork.h, TimeStatus.h, TutorialState.h) &#x20;
   * Assets/Plugins/Metro/devtodev.dll

   **For 2.0**

   * Assets/devtodev (the folder) &#x20;
   * Assets/Plugins/DevToDevOSX.bundle

   **After deleting, unpack devtodev.unitypackage version 2.1 and replace all the files. If you used an interface integration, you have to re-integrate SDK.**
5. **Unpack the devtodev.unitypackage into the project**
6. **There are 2 types of integration available:**

   **In the interface.**

   * Open the main screen of the app.
   * Open Window/devtodev menu, then you'll see the following window: &#x20;

![](/files/-LnlSmCGNYsI3KhmuCJT)

* Switch on "Analytics" by pressing "On" button
* Add AppKey and SecretKey for all the using platforms (you can select the platform by clicking on it). If you need to debug, switch logging on.

  App ID and Secret key can be found in the application settings (Open "Settings" → "SDK" → "Integration").

![](/files/-LnlSmCI4J8VYgTWd7vi)

* Script with all needed parameters if SDK initialization and tracking the user session will be automatically created and added to the scene.

**Using code.** \
Add the following strings to the GameObject which will be on the scene during the whole cycle of application work:

```csharp
public class YourBehaviourScript : MonoBehaviour
{
void Start() 
{
#if UNITY_ANDROID
// <param name="androidAppId"> devtodev App ID for Google Play version of application </param>
// <param name="androidAppSecret"> devtodev Secret key for Google Play version of application </param>
   DevToDev.Analytics.Initialize(string androidAppId, string androidAppSecret);
#elif UNITY_IOS
// <param name="iosAppId"> devtodev App ID for App Store version of application </param>
// <param name="iosAppSecret"> devtodev Secret key for App Store version of application </param>
   DevToDev.Analytics.Initialize(string iosAppId, string iosAppSecret);
#elif UNITY_WEBGL
// <param name="webglAppId"> devtodev App ID for Web version of application </param>
// <param name="webglAppKey"> devtodev Secret key Web version of application </param>
   DevToDev.Analytics.Initialize(string webglAppId, string webglAppSecret);
#elif UNITY_STANDALONE_WIN
// <param name="winAppId"> devtodev App ID for Windows Store version of application </param>
// <param name="winAppSecret"> devtodev Secret key for Windows Store version of application </param>
   DevToDev.Analytics.Initialize(string winAppId, string winAppSecret);
#endif
}
};
```

**The appId and appSecret values are unique for each app on each platform** and can be found in the settings of appropriate app ("Settings" → "SDK" → "Integration").

### The specificity of integration on Android platform

Add following lines at the bottom of proguard config

```
-keep class com.devtodev.** { *; }
-dontwarn com.devtodev.**
```

### The specificity of integration on iOS platform

If you are planning to build an app **for iOS, you need to add libz.tbd to the XCode project settings**. This library is used by devtodev Unity SDK to compress data sent to devtodev servers. Also, you have to add **UserNotifications.framework** as an optional library.

Please add AdSupport.framework into the project for your SDK to function correctly with iOS and also add AppTrackingTransparency.framework for iOS 14.

Here’s how you can add them.

**Option 1**

1\. Create Editor folder in the Assets folder.&#x20;

![](https://lh6.googleusercontent.com/FCvQNT8STcNUIlVwWoWocXgZfWc0QUA5J0q1JUSaIuYQXH3NagmKajBYpQS16wH0f_ztzsQFtAuqoM1YCGVnIplv9gsZSNdec5Qp8BiXNTMp6VAw5MF_qxMp67ddr2V7E0DIUnzW)

2\. In the Assets folder create DevToDevPostBuild.cs script.\
The script is below:

```java
#if UNITY_IOS
using System.IO;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;
namespace DevToDev
{
   public class DevToDevPostBuild
   {
       const string APP_TARGET_NAME = "Unity-iPhone";
       [PostProcessBuildAttribute(1)]
       public static void OnPostprocessBuild(BuildTarget target, string pathToBuiltProject)
       {
           if (target != BuildTarget.iOS)
           {
               return;
           }
           iOSPostBuild(pathToBuiltProject);
       }
       private static void iOSPostBuild(string projPath)
       {
           string pbxprojPath = projPath + "/Unity-iPhone.xcodeproj/project.pbxproj";
           PBXProject proj = new PBXProject();
           proj.ReadFromString(File.ReadAllText(pbxprojPath));
           string projectGuid = proj.TargetGuidByName(APP_TARGET_NAME);
           proj.AddFrameworkToProject(projectGuid, "AdSupport.framework", true);
           // IOS 14. Xcode 12 required.
           //proj.AddFrameworkToProject(projectGuid, "AppTrackingTransparency.framework", true);
           File.WriteAllText(pbxprojPath, proj.WriteToString());
       }
   }
}
#endif

```

3\. Uncomment proj.AddFrameworkToProject(projectGuid, "AppTrackingTransparency.framework", true); if necessary.

**Option 2.**

1\. Add AdSupport.framework into the Frameworks section of the generated Unity xcodeproj project.&#x20;

2\. For iOS 14, also add AppTrackingTransparency.framework.

![](https://lh3.googleusercontent.com/cr-nMwHUmwYnnxMaDA4UY2Pqqkr2R8IlQ5rqAY5z9sfS9mHsOX8yD_6RfdR4XzYCIa2uBanzvsmR9vi3U85Cz8YINnsdFDJcBMpx8LTIvTz3ArcjMDCVmLz-FepnPTaY8F3ZwZ7I)

## Additional initialization

**If the application you integrate SDK in is a part of a cross-platform project, then the user data initialization is required.**

Since the analytics of cross-platform projects is based on a unique user (unlike the usual projects where it is based on device identifiers), you have to:

* Set a unique cross-platform user identifier (it will be used for cross-platform project data collection).
* Actualize the user data. Mostly it is about game applications where the player has a game level as a characteristic. For such projects, you need to set the current player level.

{% hint style="info" %}
We recommend you set the user identifier before SDK initialization, otherwise, the user identifier from the previous session will be used since the SDK initialization moment till the UserID property is set.

If your cross-platform application is supposed to be used without cross-platform authorization, don't use the UserID property or use the empty string ("") as the user identifier. SDK will assign the unique identifier to the user. This identifier will be used until the real cross-platform identifier assigns to the user.
{% endhint %}

```java
/// <summary> Property allows to initialize the user. 
/// It applies when SDK initialization or user relogin.</summary>
/// <param name="activeUserId">unique cross-platform user identifier (max. 64 symbols)</param>
DevToDev.Analytics.UserId = activeUserID;

/// <summary> Method sets the current user level. 
/// Using this method allows to actualize the SDK user data in game cross-platform applications.</summary>
/// <param name="level">number of current game level of the user</param>
DevToDev.Analytics.CurrentLevel(level);

/// <summary>  Property allows to set current application version.
/// Attention! This property is necessary for WEB and Windows Standalone apps only.
/// It will be ignored on other platforms.</summary>
/// <param name="version"> current version of your application</param>
DevToDev.Analytics.ApplicationVersion = version;

/// <summary> devtodev App Id and Secret key can be found in the devtodev application
/// settings page ("Settings" → "SDK" → "Integration") </summary>
DevToDev.Analytics.Initialize(string appId, string appSecret);
```

{% hint style="warning" %}
If your application allows user to re-login (changing the user during the working session of application), then the *UserID* property and *CurrentLevel* method should be called just after the authorization. You don't need to call the SDK initialization one more time.&#x20;
{% endhint %}

### Debug mode

To enable the debug mode and make SDK notifications displayed in the console use this method:

```csharp
/// <summary> Enable/Disable log</summary>
/// <param name="isEnabled">Enabled/Disabled log</param>
DevToDev.Analytics.SetActiveLog(bool isEnabled);
```

## Collecting data about the amount of sessions and their length

{% hint style="warning" %}
The data about the amount of sessions and their length is collected automatically by default.&#x20;
{% endhint %}

In case you want to control the beginning and the end of a session manually, use the methods below:

For the start of the session use the StartSession method:

```java
//Call this when the session starts or is resumed
DevToDev.Analytics.StartSession();
```

For the end of the session use the EndSession method:

```java
//Call this when the session is completed
DevToDev.Analytics.EndSession();
```

## Delivering your application to the Mac App Store

1. Read an [article](https://docs.unity3d.com/Manual/HOWTO-PortToAppleMacStore.html) and make sure you follow all the recommendations.
2. Delete a meta file from DevToDevOSX.bundle:

   ```csharp
   File.Delete (projPath + "/Contents/Plugins/DevToDevOSX.bundle/Contents.meta");
   ```
3. Rename CFBundleIdentifier in Info.plist inside the devtodev plugin, for example:

   ```csharp
   string plistPath = appPath + "/Contents/Plugins/DevToDevOSX.bundle/Contents/Info.plist";
   PlistDocument plist = new PlistDocument ();
   plist.ReadFromString (File.ReadAllText (plistPath));
   plist.root.SetString ("CFBundleIdentifier", PlayerSettings.applicationIdentifier + ".devtodev");
   File.WriteAllText (plistPath, plist.WriteToString ());
   ```
4. Sign the DevToDevOSX.bundle with the .entitlements you created earlier. To do this, type the following into the macOS Terminal:

   ```bash
   codesign -f --deep -s 'Mac Developer: Developer Name' --entitlements "yourapp.entitlements" "path/to/your.app/Contents/Plugins/DevToDevOSX.bundle"
   ```


# Mac OS

{% hint style="danger" %}
**This generation of SDK is deprecated and is no longer supported.**\
Information about the [current version can be found here](/integration/integration-of-sdk-v2/sdk-integration/macos).
{% endhint %}

Please do the following to integrate your application with devtodev:

1. Add the application to the Space using the wizard for adding application.
2. [Download the latest version of the devtodev.framework from the GitHub repository](https://github.com/devtodev-analytics/macos-sdk) and add it to 'Linked Frameworks and Libraries' list in the general settings of the project.

   ![](/files/-LnlSlu589V0kE66lPjJ)
3. Add init method into didFinishLaunchingWithOptions method of your AppDelegate.m

   App ID and Secret key can be found in the application settings (Open "Settings" → "SDK" → "Integration").

```objectivec
  #import "AppDelegate.h"
  #import <devtodev/DevToDev.h>
  @interface AppDelegate ()
  @end
  @implementation AppDelegate
  - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
          [DevToDev initWithKey:@"appKey" andSecretKey:@"secretKey"];
  }
  @end
```

**If the application you integrate SDK in is a part of a cross-platform project, then the user data initialization is required.**

Since the analytics of cross-platform projects is based on a unique user (unlike the usual projects where it is based on device identifiers), you have to:

* Set the unique cross-platform user identifier (it will be used for a cross-platform project data collection).
* Actualize the user data. Mostly it is about game applications where the player has a game level as a characteristic. For such projects, you need to set the current player level.

```objectivec
/**
* Method allows to initialize the user. It applies when SDK initialization or user relogin.
* @param String activeUserId - unique cross-platform user identifier (max. 64 symbols)
*/
[DevToDev setUserID:@"activeUserId"];

/**
* Method sets the current user level. Using this method allows to actualize the SDK user data
* in game cross-platform applications.
* @param NSUInteger level - number of current game level of the user
*/
[DevToDev setCurrentLevel:level];

/**
* devtodev App Id and Secret key can be found in the devtodev application
* settings page ("Settings" → "SDK" → "Integration")
*/
[DevToDev initWithKey:applicationId andSecretKey:secretKey];
```

{% hint style="warning" %}
If your application allows user to re-login (changing the user during the working session of application), then the *setUserID* and *setCurrentLevel* methods should be called just after the authorization. You don't need to call the SDK initialization one more time.
{% endhint %}

{% hint style="info" %}
We recommend you set the user identifier before SDK initialization, otherwise, the user identifier from the previous session will be used since the SDK initialization moment till the setUserID method call.

If your cross-platform application is supposed to be used without cross-platform authorization, don't use the setUserID method or use the empty string ("") as the user identifier. SDK will assign the unique identifier to a user. This identifier will be used until the real cross-platform identifier assigns to the user.
{% endhint %}

## **Debug mode**

To enable the debug mode and make SDK notifications displayed in the console, use this method:

```objectivec
/**
* @param BOOL isActive
*/
[DevToDev setActiveLog: (BOOL) isActive];
```


# Adobe Air

{% hint style="danger" %}
**This generation of SDK is deprecated and is no longer supported.**
{% endhint %}

Please do the following to integrate your application with devtodev:

1. Add the application to the Space using the wizard for adding application. **Attention! If your Adobe Air can be used for compilations for different platforms**, you need to add the applications in devtodev for each platform. As a result, the statistics will be gained for each platform separately.
2. [Download the latest version of devtodev SDK from the GitHub repository.](https://github.com/devtodev-analytics/air-sdk)
3. Add com.devtodev.sdk.ane library to your application
4. **For Android** add the following permissions to the MyApplication.xml file:

```markup
  <uses-permission android:name="android.permission.INTERNET"/>
  <!-- Necessary(Required for sending analytics data to our server) -->
  <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
  <!-- Additional (Required for devices' MAC addresses collection) -->
  <uses-permission android:name="android.permission.READ_PHONE_STATE" />
  <!-- Additional (Required for cellular operator data collection) -->
```

To automatically gather the referrals data on Android, add the following strings into tag

```markup
  <receiver android:name="com.devtodev.InstallReceiver" android:enabled="true" android:exported="true">
  <intent-filter>
       <action android:name="com.android.vending.INSTALL_REFERRER" />
  </intent-filter>
  </receiver>
```

For other platforms no changes are needed.&#x20;

5\. Add the following imports to your source

```javascript
  import com.devtodev.sdk.core.DevToDev;
  import com.devtodev.sdk.core.data.consts.AccrualType;
  import com.devtodev.sdk.core.data.consts.Gender;
  import com.devtodev.sdk.core.data.consts.SocialNetwork;
  import com.devtodev.sdk.core.data.consts.TutorialState;
  import com.devtodev.sdk.cheat.data.consts.VerifyStatus;
  import com.devtodev.sdk.cheat.data.consts.TimeStatus;
  import com.devtodev.sdk.core.data.metrics.aggregated.events.CustomEventParams;
```

6\. Add following source into initialize event in MyApplication.mxml file:

```javascript
  DevToDev.init(AppId:String, SecretKey:String);
```

**The appKey and appSecret values are unique for each app on each platform** and can be found in the settings of appropriate app ("Settings" → "SDK" → "Integration").

For example:\
file MyApplication.mxml:

```markup
<?xml version="1.0" encoding="utf-8"?>
<s:Application initialize="application1_activateHandler(event)" 
               deactivate="application1_deactivateHandler(event)" 
               xmlns:fx="http://ns.adobe.com/mxml/2009" 
               xmlns:s="library://ns.adobe.com/flex/spark" applicationDPI="160">
    <fx:Script>
        <![CDATA[
                 import com.devtodev.sdk.core.DevToDev;
                 import com.devtodev.sdk.core.data.consts.AccrualType;
                 import com.devtodev.sdk.core.data.consts.Gender;
                 import com.devtodev.sdk.core.data.consts.SocialNetwork;
                 import com.devtodev.sdk.core.data.consts.TutorialState;
                 import com.devtodev.sdk.cheat.data.consts.VerifyStatus;
                 import com.devtodev.sdk.cheat.data.consts.TimeStatus;
                 import com.devtodev.sdk.core.data.metrics.aggregated.events.CustomEventParams;

                 protected function application1_activateHandler(event:Event):void {
                      DevToDev.init(AppId, SecretKey);
                      DevToDev.startSession();
                 }

                 protected function application1_deactivateHandler(event:Event):void {
                      DevToDev.endSession();        
                 }
        ]]>
    </fx:Script>

    <fx:Declarations>
    </fx:Declarations>

    <s:VGroup>
    </s:VGroup>

</s:Application>
```

file MyApplication.xml:

```markup
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<application xmlns="http://ns.adobe.com/air/application/17.0">
   <id>com.my.application</id>
   <filename>myapplication</filename>
   <name>myapplication</name>
   <versionNumber>1.0.0</versionNumber>
   <initialWindow>
      <autoOrients>true</autoOrients>
      <fullScreen>false</fullScreen>
      <visible>true</visible>
      <softKeyboardBehavior>none</softKeyboardBehavior>
   </initialWindow>
   <android>
      <colorDepth>16bit</colorDepth>
      <manifestAdditions><![CDATA[
         <manifest android:installLocation="auto">
            <uses-permission android:name="android.permission.INTERNET"/>
            <uses-permission android:name="android.permission.WAKE_LOCK"/>
            <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
            <application>
                 <receiver android:name="com.devtodev.InstallReceiver" android:enabled="true"
                 android:exported="true">
                   <intent-filter>
                        <action android:name="com.android.vending.INSTALL_REFERRER" />
                   </intent-filter>
                 </receiver>
            </application>
         </manifest>
      ]]></manifestAdditions>
   </android>
   <iPhone>
      <InfoAdditions><![CDATA[
         <key>UIDeviceFamily</key>
         <array>
            <string>1</string>
            <string>2</string>
         </array>
      ]]></InfoAdditions>
      <requestedDisplayResolution>high</requestedDisplayResolution>
   </iPhone>

   <extensions>
      <extensionID>com.devtodev.SDK</extensionID>
   </extensions>
</application>
```

## Additional initialization

If the application you integrate SDK in is a part of cross-platform project, then the user data initialization is required.

Since the analytics of cross-platform projects is based on a unique user (unlike the usual projects where it is based on device identifiers), you have to:

* Set the unique cross-platform user identifier (it will be used for cross-platform project data collection).
* Actualize the user data. Mostly it is about game applications where the player has a game level as a characteristic. For such projects, you need to set the current player level.

{% hint style="info" %}
We recommend you set the user identifier before SDK initialization, otherwise, the user identifier from the previous session will be used since the SDK initialization moment till the setUserId method call.

If your cross-platform application is supposed to be used without cross-platform authorization, don't use the setUserId method or use the empty string ("") as the user identifier. SDK will assign the unique identifier to the user. This identifier will be used until the real cross-platform identifier assigns to the user.
{% endhint %}

```javascript
/**
* Method allows to initialize the user. It applies when SDK initialization or user relogin.
* @param activeUserId - unique cross-platform user identifier (max. 64 symbols)
*/
DevToDev.setUserId(activeUserId:String);

/**
* Method sets the current user level. Using this method allows to actualize the SDK user data
* in game cross-platform applications.
* @param level - number of current game level of the user
*/
DevToDev.setCurrentLevel(level:int);

/**
* devtodev App Id and Secret key can be found in the devtodev application
* settings page ("Settings" → "SDK" → "Integration")
* @param appKey - application key
* @param appSecret - application secret
*/
DevToDev.init(appKey:String, appSecret:String);
```

{% hint style="warning" %}
If your application allows user to re-login (changing the user during the working session of application), then the *setUserID* and *setCurrentLevel* methods should be called just after the authorization. You don't need to call the SDK initialization one more time.&#x20;
{% endhint %}

## Debug mode

To enable the debug mode and make SDK notifications displayed in the console, use this method:

```javascript
/**
* @param logLevel (set logLevel=1 to enable log, 0 to disable)
*/
DevToDev.setLogLevel(logLevel:int);
```


# UE4

{% hint style="danger" %}
**This generation of SDK is deprecated and is no longer supported.**\
Information about the [current version can be found here](/integration/integration-of-sdk-v2/sdk-integration/unreal-engine).
{% endhint %}

Please do the following to integrate your application with devtodev:

1. Add the application to the Space using the wizard for adding application.
2. [Download the latest version of devtodev SDK from the GitHub repository.](https://github.com/devtodev-analytics/unreal-sdk)
3. Go to your project directory and place the plugin content here: ProjectName/Plugins![](/files/-LnlSlqdrnhG72DDxyYG)
4. Restart Unreal Editor and open the plugin menu (Window > Plugins). In the "Analytics" plugin group of your project select DevToDev (and "Blueprint Analytics Framework" in case if you use blueprints). You will be offered to restart Unreal Editor again.\
   ![](/files/-LnlSlqfVQo4t3rAAM8J)
5. Finally, add the following strings into the DefaultEngine.ini configuration file (the file is in "Config" folder of your project):

```
[Analytics]
ProviderModuleName=DevToDev
```

## Project Settings

To get an access to the DevToDev settings, go to **Project Settings > DevToDev**

![](/files/-LnlSlqhHvEpuPil4p-Z)

Get the keys (they can be found in the application settings: Settings -> SDK -> Integration) and insert the keys in this window. Then choose the Enable Push Notifications option in case if you want to send push notifications through devtodev service.

## Initialization

All the events are available in the Analytics block of your Blueprint.

![](/files/-LnlSlqjnRHkbHPujmNn)

To initialize SDK in a blueprint, first call the "Start Session" event from the Analytics Blueprint Library.

![](/files/-LnlSlqlnuQL518ezB08)

or from the following code

```cpp
FAnalytics::Get().GetDefaultConfiguredProvider()->StartSession();
```


# Setting up Events


# Basic methods

{% hint style="danger" %}
**This generation of SDK is deprecated and is no longer supported.**\
Information about the [current version can be found here](/integration/integration-of-sdk-v2/setting-up-events/basic-methods).
{% endhint %}

[Expert tips](/integration/expert-tips/what-to-track) before integrating the events.

## **Onboarding (tutorial)**

The Tutorial steps event allows you to evaluate the effectiveness of the tutorial steps system. The event should be sent at the end of each tutorial step indicating the number of every passed step as a parameter.

Use the following constants to specify basic events of tutorial steps:

{% tabs %}
{% tab title="iOS" %}

* **Start** or **-1** - at the beginning, before the first step is completed;
* **Finish** or **-2** - instead of the final step number;
* **Skipped** or **0** - in case а user skipped the tutorial.
  {% endtab %}

{% tab title="Android" %}

* DevToDev.TutorialState.**Start** or **-1** - at the beginning, before the first step is completed;
* DevToDev.TutorialState.**Finish** or **-2** - instead of the last step number;
* DevToDev.TutorialState.**Skipped** or **0** - if a user skipped the tutorial.
  {% endtab %}

{% tab title="Windows 8.1 and 10" %}

* DevToDev.TutorialState.**Start** or **-1** - at the beginning, before the first step is completed;
* DevToDev.TutorialState.**Finish** or **-2** - instead of the last step number;
* DevToDev.TutorialState.**Skipped** or **0** - if a user skipped the tutorial.
  {% endtab %}

{% tab title="Web" %}

* **-1** - Start the tutorial (at the beginning, before the first step is completed)
* **-2** - Tutorial finished (instead of the last step number)
* **0** - Tutorial skipped (if a user skipped the tutorial).
  {% endtab %}

{% tab title="Unity" %}

* DevToDev.TutorialState.**Start** or **-1** - at the beginning, before the first step is completed;
* DevToDev.TutorialState.**Finish** or **-2** - instead of the last step number;
* DevToDev.TutorialState.**Skipped** or **0** - if a user skipped the tutorial.
  {% endtab %}

{% tab title="Mac OS" %}

* **Start** or **-1** - at the beginning, before the first step is completed;
* **Finish** or **-2** - instead of the final step number;
* **Skipped** or **0** - in case a user skipped the tutorial.
  {% endtab %}

{% tab title="Adobe Air" %}

* TutorialState.**START** or **-1** - at the beginning, before the first step is completed;
* TutorialState.**FINISH** or **-2** - instead of the last step number;
* TutorialState.**SKIPPED** or **0** - if a user skipped the tutorial.
  {% endtab %}

{% tab title="UE4" %}

* **-1 (Start)** - at the beginning, before the first step is completed;
* **-2 (Finish)** - instead of the number of the last step;
* **0 (Skipped)** - in case a user skipped the tutorial.
  {% endtab %}
  {% endtabs %}

In other cases use step numbers. Make sure you use numbers above 0 to enumerate the steps.

{% hint style="warning" %}
The logic of the use of the **Skipped** constant in the Tutorial steps event is provided only in case a user has completely refused to pass the tutorial. After **Skipped** is used, no other values of the Tutorial steps event must be received.&#x20;
{% endhint %}

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* The event allowing to track the stage of tutorial a player is on.
* @param NSUInteger tutorialStep - the latest successfully completed tutorial step.
*/
[DevToDev tutorialCompleted: (NSUInteger) tutorialStep];
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* The event allowing to track the stage of tutorial a player is on.
* @param int tutorialStep - the latest successfully completed tutorial step.
*/
DevToDev.tutorialCompleted(int tutorialStep);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
*  <param name="state"> The latest successfully completed tutorial step </param>
*/
DevToDev.SDK.Tutorial(int state)
```

{% endtab %}

{% tab title="Web" %}

```javascript
/**
* The event allowing to track the stage of tutorial a player is on.
* @param {number} tutorialStep - the latest successfully completed tutorial step.
*/
devtodev.tutorialCompleted(tutorialStep);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <summary> The event allowing to track the stage of tutorial a player is on. </summary>
/// <param name="tutorialStep"> The latest successfully completed tutorial step </param>
DevToDev.Analytics.Tutorial(int tutorialStep);
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* The event allowing to track the stage of tutorial a player is on.
* @param NSUInteger tutorialStep - the latest successfully completed tutorial step.
*/
[DevToDev tutorialCompleted: (NSUInteger) tutorialStep];
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* The event allowing to track the stage of tutorial a player is on.
* @param tutorialStep - the latest successfully completed tutorial step.
*/
DevToDev.tutorialCompleted(tutorialStep:int);
```

{% endtab %}

{% tab title="UE4" %}
**Blueprint**

![](https://blobscdn.gitbook.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LnBolWcwPO33iga2rY2%2F-LnTASe7tY_8XGfgeRsv%2F-LnTAVQIcZXYkBhcsAJX%2F142-basicmethods-0.png?generation=1567095664522623\&alt=media)

| Field | Type  | Description                                     |
| ----- | ----- | ----------------------------------------------- |
| Step  | int32 | The latest successfully completed tutorial step |

**Code**

```
// The event allowing to track the stage of tutorial a player is on.
// int32 step - the latest successfully completed tutorial step.

UDevToDevBlueprintFunctionLibrary::TutorialCompleted(int32 step);
```

{% endtab %}
{% endtabs %}

## **Leveling up**

This event is for games only.

You can analyze the distribution of the players over the levels. The event should be sent right after the player reached the next level. You can find more information on what is the right moment to use LevelUp event [here](/integration/expert-tips/what-to-track#if-users-in-your-project-become-more-experienced-and-raise-their-level).

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* Player has reached a new level
* @param NSInteger level - level reached by the player.
*/
[DevToDev levelUp: (NSInteger) level];
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* Player has reached a new level
* @param int level - level reached by the player.
*/
DevToDev.levelUp(int level);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
* <param name="level"> Level reached by the player </param>
*/
DevToDev.SDK.Level(int level);
```

{% endtab %}

{% tab title="Web" %}

```javascript
/**
* Player has reached a new level
* @param {number} level - level reached by the player.
*/

devtodev.levelUp(level);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <summary> Player has reached a new level </summary>
/// <param name="level"> Level reached by the player </param>
DevToDev.Analytics.LevelUp(int level);
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* Player has reached a new level
* @param NSUInteger level - level reached by the player.
*/
[DevToDev levelUp: (NSUInteger) level];
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* Player has reached a new level
* @param level - level reached by the player.
*/
DevToDev.levelUp(level:int);
```

{% endtab %}

{% tab title="UE4" %}

### Blueprint

![](/files/-LnlSi7nFoLb80mawRaA)

| Field | Type  | Description                 |
| ----- | ----- | --------------------------- |
| Level | int32 | level reached by the player |

**Code**

```cpp
// Player has reached a new level
// int32 level - level reached by the player.

UDevToDevBlueprintFunctionLibrary::LevelUp(int32 level);
```

{% endtab %}
{% endtabs %}

To track the average account balance of in-game currency by the end of each level, please provide the list of currency names and amounts.

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* Player has reached a new level
* @param NSInteger level - level reached by the player.
* @param NSDictionary resources - dictionary with the currency names and amounts
*/
NSDictionary * resources = @{@"Currency name 1" : @100, @"Currency name 2" : @10};
[DevToDev levelUp: (NSUInteger) level withResources: withResources: (NSDictionary *) resources];
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* @param int level - level reached by the player
* @param HashMap resources - hashmap with the currency names and amounts
*/
HashMap resources = new HashMap<String, Integer>();
resources.put("Currency name 1", 1000);
resources.put("Currency name 2", 10);
DevToDev.levelUp(level, resources);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
* <param name="level"> Player level </param>
* <param name="resources">Dictionary with the currency names and amounts</param>
*/
Dictionary<string, int> resources = new Dictionary<string, int>();
resources.Add("Currency name 1", 1000);
resources.Add("Currency name 2", 10);
DevToDev.SDK.Level(level, resources);
```

{% endtab %}

{% tab title="Web" %}

```javascript
/**
* @param {number} level - player/character's "level"
* @param {Object} resources - Object with data about currencies. Optional.
* @param {Object[]} resources.balance -  Account balance of in-game currency by the end of level. * Optional.
* @param {Object[]} resources.balance[].currency - Game currency name
* @param {Object[]} resources.balance[].amount - Game currency amount
* @param {Object[]} resources.earned - Game currency earned during the level. Optional. 
* @param {Object[]} resources.earned[].currency - Game currency name
* @param {Object[]} resources.earned[].amount - Game currency amount
* @param {Object[]} resources.spent - Game currency amount spent during the level. Optional.
* @param {Object[]} resources.spent[].currency - Game currency name
* @param {Object[]} resources.spent[].amount - Game currency amount
* @param {Object[]} resources.bought - Game currency amount bought during the level. Optional.
* @param {Object[]} resources.bought[].currency - Game currency name
* @param {Object[]} resources.bought[].amount - Game currency amount 
*/

devtodev.levelUp(level, resources);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <summary> Player has reached a new level</summary>
/// <param name="level"> Player level </param>
/// <param name="resources"> Dictionary with the currency names and amounts </param>
/// <example>
/// 
///    Dictionary<string, int> resources = new Dictionary<string, int>();
///    resources.Add("Currency name 1", 1000);
///    resources.Add("Currency name 2", 10);
/// 
/// </example>
DevToDev.Analytics.LevelUp(level, resources);
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* Player has reached a new level
* @param NSInteger level - level reached by the player.
* @param NSDictionary resources - dictionary with the currency names and amounts
*/
NSDictionary * resources = @{@"Currency name 1" : @100, @"Currency name 2" : @10};
[DevToDev levelUp: (NSUInteger) level withResources: withResources: (NSDictionary *) resources];
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* Player has reached a new level
* @param level - level reached by the player.
* @param resources - dictionary with the currency names and amounts
*/
var resources: Dictionary = new Dictionary();
resources["Currency name 1"] = 1000;
resources["Currency name 2"] = 10;
DevToDev.levelUpWithResources(level:int, resources:Dictionary);
```

{% endtab %}

{% tab title="UE4" %}
**Blueprint**

![](/files/-LnlSi7yLjdnsXuFgGba)

| Field | Type  | Description                 |
| ----- | ----- | --------------------------- |
| Level | int32 | level reached by the player |

```cpp
// Player has reached a new level
// int32 level - level reached by the player.
// TArray<FAnalyticsEventAttr> Attributes - dictionary with the currency names and amounts

UDevToDevBlueprintFunctionLibrary::LevelUpWithAttributes(int32 level,
                                                         const TArray<FAnalyticsEventAttr>& Attributes);
```

{% endtab %}
{% endtabs %}

To track the average amount of in-game currency earned during a level, it is necessary to send a special event after each time an in-game account is replenished.

{% tabs %}
{% tab title="iOS" %}

```objectivec
/* *
* @param NSString * currencyName - currency name (max. 24 symbols)
* @param NSInteger amount - the amount an account has been credited with.
* @param AccrualType accrualType - the way the currency was obtained: earned or purchased
*/
[DevToDev currencyAccrual: (NSInteger) amount withCurrencyName: (nonnull NSString *) currencyName
 andCurrencyType: (AccrualType) accrualType];
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* @param String currencyName - currency name (max. 24 symbols)
* @param float currencyAmount - the amount an account has been credited with
* @param AccrualType accrualType - the way the currency was obtained: earned or purchased
*/
DevToDev.currencyAccrual(currencyName, currencyAmount, accrualType);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
* <param name="currencyName">Currency name (max. 24 symbols)</param>
* <param name="currencyAmount ">The amount an account has been credited with</param>
* <param name="accrualType">The way the currency was obtained: earned or purchased</param>
*/
DevToDev.SDK.CurrencyAccrual(string currencyName, float currencyAmount, AccrualType accrualType);
```

{% endtab %}

{% tab title="Web" %}

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <param name="currencyName"> Currency name (max. 24 symbols) </param>
/// <param name="currencyAmount "> The amount an account has been credited with </param>
/// <param name="accrualType"> The way the currency was obtained: earned or purchased </param>
DevToDev.Analytics.CurrencyAccrual(int amount, string currencyName, AccrualType accrualType);
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* @param NSString * currencyName - currency name (max. 24 symbols)
* @param float amount - the amount an account has been credited with.
* @param AccrualType accrualType - the way the currency was obtained: earned or purchased
*/
/* *
* @param NSString * currencyName - currency name (max. 24 symbols)
* @param NSInteger amount - the amount an account has been credited with.
* @param AccrualType accrualType - the way the currency was obtained: earned or purchased
*/
[DevToDev currencyAccrual: (NSInteger) amount withCurrencyName: (nonnull NSString *) currencyName
 andCurrencyType: (AccrualType) accrualType];
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* @param currencyName - currency name (max. 24 symbols)
* @param currencyAmount - the amount an account has been credited with
* @param accrualType - the way the currency was obtained: earned or purchased
*/
DevToDev.currencyAccrual(currencyName:String, currencyAmount:Number, accrualType:AccrualType);
```

{% endtab %}

{% tab title="UE4" %}

### Blueprint

![](/files/-LnlSi88D1on0dEqfmNi)

| Field                | **Type** | **Description**                                           |
| -------------------- | -------- | --------------------------------------------------------- |
| Game Currency Type   | FString  | Currency name (max. 24 symbols)                           |
| Game Currency Amount | int32    | The amount an account has been credited with.             |
| accrualType          | Enum     | Can take one of following values: "Earned" or "Purchased" |

Code

```cpp
FAnalytics::Get().GetDefaultConfiguredProvider()->RecordCurrencyGiven(const FString& GameCurrencyType,
                                                   int GameCurrencyAmount,
                                                   const TArray<FAnalyticsEventAttribute>& EventAttrs);
```

{% endtab %}
{% endtabs %}

AccrualType can take one of the following values:

{% tabs %}
{% tab title="iOS" %}

```objectivec
typedef enum {
Earned,
Purchased
} AccrualType ;
```

{% endtab %}

{% tab title="Android" %}

```java
public enum AccrualType {
                          Earned,
                          Purchased
                        };
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
public enum AccrualType {
                          Earned,
                          Purchased
};
```

{% endtab %}

{% tab title="Web" %}

{% endtab %}

{% tab title="Unity" %}

```csharp
public enum AccrualType {
    Earned,
    Purchased
};
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
typedef enum {
              Earned,
              Purchased
} AccrualType;
```

{% endtab %}

{% tab title="Adobe Air" %}

```java
AccrualType.EARNED;
AccrualType.PURCHASED;
```

{% endtab %}

{% tab title="UE4" %}

{% endtab %}
{% endtabs %}

## Real-World Currency Payment

To track payments, add this event right after the platform confirms that a payment went through.

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* Register transactions made through the platform's payment system.
*
* @param NSString * paymentId - transaction ID  (max. 64 symbols)
* @param float inAppPrice - product price (in user's currency)
* @param NSString * inAppName - product name
* @param NSString * inAppCurrencyISOCode - transaction currency (ISO 4217 format)
*/
[DevToDev realPayment: (NSString *) transactionId withInAppPrice:(float) inAppPrice 
andInAppName: (NSString *) inAppName andInAppCurrencyISOCode: (NSString *) inAppCurrencyISOCode];
```

A unique order identifier is a value of a *transactionIdentifier* property in SKPaymentTransaction object inside the receipt of completed transaction.

{% hint style="warning" %}
devtodev server does not process transactions with previously used transaction IDs. Also, the server validates identifiers in appearance to avoid evident cheat transactions.\
To avoid adding cheat payments into reports completely, use devtodev anti-cheat service before creating a realPayment even&#x74;*.*
{% endhint %}
{% endtab %}

{% tab title="Android" %}

```java
/**
* Register transactions made through the platform's payment system.
*
* @param String paymentId - transaction ID (max. 64 symbols)
* @param float inAppPrice - product price (in user's currency)
* @param String inAppName - product name
* @param String inAppCurrencyISOCode - transaction currency 
* (ISO 4217 format http://www.iso.org/iso/home/standards/currency_codes.htm Exapmle: "USD")
*/
DevToDev.realPayment(String paymentId, float inAppPrice, String inAppName, 
                     String inAppCurrencyISOCode);
```

How to find the transaction ID in GooglePlay transaction?

Find the *INAPP\_PURCHASE\_DATA* object In the JSON fields that are returned in the response data for a purchase order. A unique transaction identifier is the value of *orderId* property in *INAPP\_PURCHASE\_DATA* object. If the order is a test purchase made via the In-app Billing Sandbox, *orderId* property will be empty.

{% hint style="warning" %}
devtodev server does not process transactions with previously used transaction IDs. Also, the server validates the identifiers in appearance to avoid evident cheat transactions. To avoid completely the entering of cheat payments from GooglePlay in reports, use devtodev anticheat service before creating realPayment event.&#x20;
{% endhint %}
{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
* Register transactions made through the platform's payment system.
*  <param name="orderId"> Transaction id </param>
*  <param name="price"> Product price (in user's currency) </param>
*  <param name="productId"> Product id (product name) </param>
*  <param name="currencyCode"> Transaction currency (ISO 4217 format)</param>
*/
DevToDev.SDK.RealPayment(string orderId, float price, string productId, string currencyCode)
```

Example:

```csharp
DevToDev.SDK.RealPayment("1836535032137741465" , 2.99f , "productId" , "USD" );
```

{% endtab %}

{% tab title="Web" %}

```javascript
/**
* Register transactions made through the platform's payment system.
*
* @param {string} transactionId - transaction ID
* @param {number} productPrice - product price (in user's currency)
* @param {string} productName - product name
* @param {string} transactionCurrencyISOCode - transaction currency (ISO 4217 format)
*/

devtodev.realPayment(transactionId, productPrice, productName, transactionCurrencyISOCode);
```

Example:

```javascript
devtodev.realPayment("12345", 9.99, "Currency pack 2", "USD");
```

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <summary> Register transactions made through the platform's payment system. </summary>
/// <param name="paymentId"> Transaction id </param>
/// <param name="inAppPrice"> Product price (in user's currency) </param>
/// <param name="inAppName"> Product id (product name) </param>
/// <param name="inAppCurrencyISOCode"> Transaction currency (ISO 4217 format)</param>
DevToDev.Analytics.RealPayment(string paymentId, float inAppPrice, string inAppName,
                               string inAppCurrencyISOCode);
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* Register transactions made through the platform's payment system.
*
* @param NSString * paymentId - transaction ID
* @param float inAppPrice - product price (in user's currency)
* @param NSString * inAppName - product name
* @param NSString * inAppCurrencyISOCode - transaction currency (ISO 4217 format)
*/
[DevToDev realPayment: (NSString *) transactionId withInAppPrice:(float) inAppPrice 
         andInAppName: (NSString *) inAppName andInAppCurrencyISOCode: (NSString *) inAppCurrencyISOCode];
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* Register transactions made through the platform's payment system.
* @param paymentId - transaction ID
* @param inAppPrice - product price (in user's currency)
* @param inAppName - product name
* @param inAppCurrencyISOCode - transaction currency (ISO 4217 format)
*/
DevToDev.realPayment(paymentId:String, inAppPrice:Number, inAppName:String,
                     inAppCurrencyISOCode:String);
```

**How to find the transaction ID in iTunes transaction?**

Unique order identifier is a value of "transactionIdentifier" property in SKPaymentTransaction object inside the receipt of completed transaction.

**How to find the transaction ID in GooglePlay transaction?**

Find the INAPP\_PURCHASE\_DATA object In the JSON fields that are returned in the response data for a purchase order. A unique transaction identifier is the value of orderId property in INAPP\_PURCHASE\_DATA object. If the order is a test purchase made via the In-app Billing Sandbox, orderId property will be empty.

{% hint style="warning" %}
devtodev server does not process transactions with previously used transaction IDs. Also the server validates the identifiers in appearance, to avoid evident cheat transactions. To avoid the entering of cheat payments in reports completely, use devtodev anticheat service before creating realPayment event.&#x20;
{% endhint %}
{% endtab %}

{% tab title="UE4" %}
**Blueprint**

![](/files/-LnlSi8d2kr26vtPAC08)

| **Field**               | **type** | **Description**                        |
| ----------------------- | -------- | -------------------------------------- |
| Transaction Id          | FString  | Unique transaction ID                  |
| In  AppPrice            | float    | Product price (in user's currency)     |
| In App Name             | FString  | Product name                           |
| In App Currency ISOCode | FString  | Transaction currency (ISO 4217 format) |

Code

```cpp
// Register transactions made through the platform's payment system.
// FString transactionId - transaction ID
// float inAppPrice - product price (in user's currency)
// FString inAppName - product name
// FString inAppCurrencyISOCode - transaction currency (ISO 4217 format)

UDevToDevBlueprintFunctionLibrary::RealPayment(const FString& transactionId,
                                               float inAppPrice,
                                               const FString& inAppName,
                                               const FString& inAppCurrencyISOCode);
```

{% endtab %}
{% endtabs %}

## Virtual Currency Payment

This event is for games only.

To track expenditures of in-game currency and popularity of products, add this event right after the purchase.

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* In-app purchase with a definite article ID.
*
* @param NSString purchaseId - unique purchase Id or name (max. 32 symbols)
* @param NSString purchaseType - purchase type or group (max. 96 symbols)
* @param NSInteger purchaseAmount - count of purchased goods
* @param NSInteger purchasePrice - cost of purchased goods (total cost -if several goods were purchased)
* @param NSString purchaseCurrency - currency name (max. 24 symbols)
*/
[DevToDev inAppPurchase: (NSString *) purchaseId withPurchaseType: (NSString *) purchaseType 
andPurchaseAmount: (NSInteger) purchase Amount andPurchasePrice: (NSInteger) purchaseprice 
andPurchaseCurrency: (NSString *) purchaseCurrency];
```

In case a product is bought for several game currencies at once, it is necessary to make a dictionary that includes the names and amounts of the paid currencies.

```objectivec
NSMutableDictionary * resources = [[NSMutableDictionary alloc] init];
[resources setObject:@100 forKey:@"currency1"];
[resources setObject:@10 forKey:@"currency2"];
//...and so on...

[DevToDev inAppPurchase:(NSString )purchaseId withPurchaseType:(NSString )purchaseType
 andPurchaseAmount:(NSInteger)purchaseAmount andResources: (NSDictionary *) resources];
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* In-app purchase with a definite article ID.
*
* @param purchaseId - unique purchase Id or name (max. 32 symbols)
* @param purchaseType - purchase type or group (max. 96 symbols)
* @param purchaseAmount - count of purchased goods
* @param purchasePrice - cost of purchased goods (total cost - if several goods were purchased)
* @param purchaseCurrency - currency name (max. 24 symbols)
*/
DevToDev.inAppPurchase(String purchaseId, String purchaseType, int purchaseAmount, 
                       int purchasePrice, String purchaseCurrency);
```

In case a product was bought for several game currencies at once, it is necessary to make a hashmap that includes the names and amounts of the paid currencies.

```java
HashMap resources = new HashMap();
resources.put("currency_1", 120);
resources.put("currency_2", 29);
```

…and so on…

```java
DevToDev.inAppPurchase(String purchaseId, String purchaseType, int purchaseAmount, HashMap resources);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
* <param name="purchaseId"> Unique purchase ID or name (max. 32 symbols)</param>
* <param name="purchaseType"> Purchase type or group (max. 96 symbols)</param>
* <param name="purchaseAmount"> Number of purchased goods </param>
* <param name="purchasePrice"> Cost of purchased goods (total cost - if several goods were purchased)</param>
* <param name="purchasePriceCurrency"> Currency name (max. 24 symbols)</param>
*/
DevToDev.SDK.InAppPurchase(string purchaseId, string purchaseType, int purchaseAmount,
                           int purchasePrice, string purchasePriceCurrency)
```

Example:

```csharp
DevToDev.SDK.InAppPurchase("sword", "weapons", 1, 200, "coins");
```

In case a product was bought for several game currencies at once, it is necessary to make a hashmap including the names and amounts of the paid currencies.

```csharp
Dictionary<string, int> resources = new Dictionary<string, int>();
resources.Add("currency_1", 120);
resources.Add("currency_2", 29);
//...and so on...

DevToDev.SDK.InAppPurchase(string purchaseId, string purchaseType, int purchaseAmount, 
                           Dictionary<string, int> resources);
```

{% endtab %}

{% tab title="Web" %}

```javascript
/**
* Tracks in-app purchases.
*
* @param {string} purchaseId - unique purchase Id or name (max. 32 symbols)
* @param {string} purchaseType - purchase type or group (max. 96 symbols)
* @param {number} purchaseAmount - count of purchased goods
* @param {Object[]} purchasePrice - array including the names and amounts of
* the paid currencies (total cost - if several goods were purchased)
* @param {string} purchasePrice[].currency - game currency name
* @param {number} purchasePrice[].amount - currency amount
*/

devtodev.inAppPurchase(purchaseId, purchaseType, purchaseAmount, purchasePrice);
```

Example:

```javascript
var purchasePrice = [
    {
        “currency” : "coins", //game currency name
        “amount” : 1000 //game currency amount 
    },
    {
        “currency” : "gold", //game currency name
        “amount” : 10 //game currency amount
    }
];

devtodev.inAppPurchase(“cloak”, “clothes”, 1, purchasePrice);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <summary> In-app purchase with a definite ID. </summary>
/// <param name="purchaseId"> Unique purchase ID  or name (max. 32 symbols)</param>
/// <param name="purchaseType"> Purchase type or group (max. 96 symbols)</param>
/// <param name="purchaseAmount"> Number of purchased goods </param>
/// <param name="purchasePrice"> Cost of purchased goods (total cost - if several goods were purchased)</param>
/// <param name="purchasePriceCurrency"> Currency name (max. 24 symbols)</param>
DevToDev.Analytics.InAppPurchase(string purchaseId, string purchaseType, int purchaseAmount,
                                 int purchasePrice, string purchaseCurrency);
```

In case a product was bought for several game currencies at once, it is necessary to make a dictionary including the names and amounts of the paid currencies.

```csharp
Dictionary<string, int> resources = new Dictionary<string, int>();
resources.Add("currency_1", 120);
resources.Add("currency_2", 29);
//...and so on...

DevToDev.Analytics.InAppPurchase(string purchaseId, string purchaseType, int purchaseAmount,
                                 Dictionary<string, int> resources);
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* In-app purchase with a definite article ID.
*
* @param purchaseId - unique purchase Id or name (max. 32 symbols)
* @param purchaseType - purchase type or group (max. 96 symbols)
* @param purchaseAmount - count of purchased goods
* @param purchasePrice - cost of purchased goods (total cost -if several goods were purchased)
* @param purchaseCurrency - currency name (max. 24 symbols)
*/
[DevToDev inAppPurchase: (NSString *) purchaseId withPurchaseType: (NSString *) purchaseType
      andPurchaseAmount: (NSInteger) purchase Amount andPurchasePrice: (NSInteger) purchaseprice 
    andPurchaseCurrency: (NSString *) purchaseCurrency];
```

In case a product was bought for several game currencies at once, it is necessary to make a dictionary including the names and amounts of the paid currencies.

```objectivec
NSMutableDictionary * resources = [[NSMutableDictionary alloc] init];
[resources setObject:@100 forKey:@"currency1"];
[resources setObject:@10 forKey:@"currency2"];
//...and so on...

[DevToDev inAppPurchase:(NSString )purchaseId withPurchaseType:(NSString )purchaseType 
      andPurchaseAmount:(NSInteger)purchaseAmount andResources: (NSDictionary *) resources];
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* In-app purchase with a definite article ID.
* @param purchaseId - unique purchase Id or name (max. 32 symbols)
* @param purchaseType - purchase type or group (max. 96 symbols)
* @param purchaseAmount - count of purchased goods
* @param purchasePrice - cost of purchased goods (total cost - if several goods were purchased)
* @param purchaseCurrency - currency name (max. 24 symbols)
*/
DevToDev.inAppPurchase(purchaseId:String, purchaseType:String, purchaseAmount:int, purchasePrice:int,
                       purchaseCurrency:String);
```

In case a product was bought for several game currencies at once, it is necessary to make a hashmap including the names and amounts of the paid currencies.

```javascript
var resources: Dictionary = new Dictionary();
resources["currency_1"] = 120;
resources["currency_2"] = 29;
//...and so on...

DevToDev.inAppPurchaseWithResources(purchaseId:String, purchaseType:String, purchaseAmount:int,
                                    resources:Dictionary);
```

{% endtab %}

{% tab title="UE4" %}
**Blueprint**

![](/files/-LnlSi9QC0ZIJXCM2D6i)

Notice! If the purchase is done by more than one currency, then the method should be called as many times as many currencies were used, but the amount of purchase should be set only in one of the times.

Use the method “Record Simple Item Purchase with Attributes” from Analytics Blueprint Library.

| **Field**     | **Type** | **Description**                              |
| ------------- | -------- | -------------------------------------------- |
| Item Id       | FString  | Unique purchase Id or name (max. 32 symbols) |
| Item Quantity | int32    | Count of purchased goods                     |

**Item Id** field is the identifier of purchased item, **Item Quantity** is the amount of purchased item. Attributes array should contain the following obligatory information:

| **Field**        | **Type** | **Description**                                                       |
| ---------------- | -------- | --------------------------------------------------------------------- |
| purchaseType     | FString  | Purchase type or group (max. 96 symbols)                              |
| purchasePrice    | int32    | Cost of purchased goods (total cost -if several goods were purchased) |
| purchaseCurrency | FString  | Currency name (max. 24 symbols)                                       |

Code

```cpp
// In-app purchase with a definite article ID.
// FString ItemId - unique purchase Id or name (max. 32 symbols)
// int32 ItemQuantity - count of purchased goods
//
// FString purchaseType - purchase type or group (max. 96 symbols)
// int32 purchasePrice - cost of purchased goods (total cost -if several goods were purchased)
// FString purchaseCurrency - currency name (max. 24 symbols)

FAnalytics::Get().GetDefaultConfiguredProvider()->RecordItemPurchase(const FString& ItemId,
                                                   int ItemQuantity,
                                                   const TArray<FAnalyticsEventAttribute>& Attributes);
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Please keep in mind that there is a limit for the number of unique values of the "purchaseCurrency" parameter - 30 currencies per project. Currencies cannot be deleted or renamed.
{% endhint %}

## **Custom Events**

If you want to count the events that are not among basic, use custom events.

{% hint style="warning" %}
Attention! We strongly recommend that you do not use custom event properties to transfer and store data that fits the definition of [personal data](https://gdpr-info.eu/issues/personal-data/)!
{% endhint %}

The event must have a unique name and can include up to 20 parameters. The maximum length of the event name is 72 symbols.

Every parameter inside one event must have a unique name. The maximum length of the parameter name is 32 symbols.

The values of parameters can be string or number type (int, long, float, double). The maximum length of the parameter value is 255 symbols.

{% hint style="warning" %}
No more than 300 variants of custom event names can be used for one project. Try to enlarge events in meaning by using event parameters. Events that didn't get into the limit of unique event names will be discarded.&#x20;
{% endhint %}

For a string parameter, it is acceptable to use not more than 50,000 unique values for the whole event history. In case the limit of unique values is exceeded, the parameter is ignored.

Therefore, we recommend not to set user IDs and Unix time as parameter values of custom events. Try to integrate parameter values if they have a very large variability. Otherwise, it will be very difficult to analyze the data or after some time it may be even ignored.

We strongly recommend not to change the type of data transferred in the parameter over time. In case you change the data type in parameter, it will be duplicated with the same name and different data types in devtodev database which will result in more complicated report building.

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* @param NSString eventName - event name
*/
[DevToDev customEvent: (NSString *) eventName];
```

20 parameter names may be associated with any event:

```objectivec
CustomEventParams * params_1 = [[CustomEventParams alloc] init];
[params_1 putParam:@"double" withDouble:123.1231231231231];
[params_1 putParam:@"float" withFloat:123.123123f];
[params_1 putParam:@"int" withInt:123];
[params_1 putParam:@"long" withLong:6152437L];
[param_1 putParam:@"string" withString:@"string"];
```

Then use method:

```objectivec
/**
* @param NSString eventName - event name
* @param CustomEventParams params - event parameters
*/
[DevToDev customEvent: (NSString *) eventName withParams: (CustomEventParams *) params];
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* Simple custom event
* @param String eventName - event name
*/
DevToDev.customEvent(eventName);
```

20 parameter names may be associated with any event:

```java
CustomEventParams params = new CustomEventParams();
params.putDouble("double", 1.12);
params.putFloat("float", 9.99f);
params.putInteger("int", 145);
params.putLong("long", 123L);
params.putString("string","start");
```

Then use method:

```java
/**
* Custom event with params
* @param String eventName - event name
* @param CustomEventParams params - event parameters
*/
DevToDev.customEvent(eventName, params);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
* <param name="eventName"> Event name </param>
*/
DevToDev.SDK.CustomEvent(string eventName);
```

Example:

```csharp
DevToDev.SDK.CustomEvent("bonus_used");
```

20 parameter names may be associated with any event:

```csharp
/**
* <param name="eventName">Event name</param>
* <param name="eventParams">Event parameters</param>
*/
DevToDev.SDK.CustomEvent(string eventName, CustomEventParams eventParams)
```

Example:

```csharp
var cep = new CustomEventParams();
cep.AddParam("bonus_name", "your_awesome_bonus");
DevToDev.SDK.CustomEvent("bonus_used", cep);
```

{% endtab %}

{% tab title="Web" %}

```javascript
/**
* Tracks custom events.
* @param {string} eventName - event name
**/

devtodev.customEvent(eventName);
```

20 parameter names may be associated with any event:

Then use method:

```javascript
/**
* Tracks custom events.
* @param {string} eventName - event name (max. 72 symbols)
* @param {Object[]} params - array of event parameters. Up to 20 params.
* @param {string} params[].name - parameter name (max. 32 symbols)
* @param {string} params[].type - parameter value type. Can be "double" or "string".
* @param {string|number} params[].value - parameter value. (max. 255 symbols)
**/

devtodev.customEvent(eventName, params);
```

Example:

```javascript
var params = [
    {
        "name": "score",
        "type": "double",
        "value": 100500,
    },
    {
        "name": "type",
        "type": "string",
        "value": "fatality",
    },
    … //up to 10 parameters.
];

devtodev.customEvent("win", params);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <param name="eventName"> Event name </param>
DevToDev.Analytics.CustomEvent(string eventName);
```

20 parameter names may be associated with any event:

```csharp
DevToDev.CustomEventParams customEventParams = new DevToDev.CustomEventParams();
customEventParams.AddParam("double", 1.12);
customEventParams.AddParam("int", 145);
customEventParams.AddParam("long", 123L);
customEventParams.AddParam("string","start");
```

Then use method:

```csharp
/// <param name="eventName">Event name</param>
/// <param name="eventParams">Event parameters</param>
DevToDev.Analytics.CustomEvent(string eventName, DevToDev.CustomEventParams eventParams);
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* @param String eventName - event name
*/
[DevToDev customEvent: (NSString *) eventName];
```

20 parameter names may be associated with any event:

```objectivec
CustomEventParams * params_1 = [[CustomEventParams alloc] init];
[params_1 putParam:@"date" withDate:[NSDate date]];
[params_1 putParam:@"double" withDouble:123.1231231231231];
[params_1 putParam:@"float" withFloat:123.123123f];
[params_1 putParam:@"int" withInt:123];
[params_1 putParam:@"long" withLong:6152437L];
[param_1 putParam:@"string" withString:@"string"];
```

Then use method:

```objectivec
/**
* @param String eventName - event name
* @param CustomEventParams params - event parameters
*/
[DevToDev customEvent: (NSString *) eventName withParams: (CustomEventParams *) params];
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* Simple custom event
* @param String eventName - event name
*/
DevToDev.customEvent(eventName:String);
```

20 parameter names may be associated with any event:

```javascript
var params:CustomEventParams = new CustomEventParams();
/**
* String type custom event parameter
* @param paramName - parameter name
* @param value - parameter value
*/
params.putString(paramName:String , value:String);
//Integer type custom event parameter
params.putInt(paramName:String, 145:int);
//Number type custom event parameter
params.putFloat(paramName:String, 9.99:Number);
```

Then use method:

```javascript
/**
* Custom event with params
* @param eventName - event name
* @param params - event parameters
*/
DevToDev.customEventsWithParams(eventName:String, params:CustomEventParams);
```

{% endtab %}

{% tab title="UE4" %}
![](/files/-LnlSiAP3Dij5ICprOkV)

| Field      | **Type** | **Description**   |
| ---------- | -------- | ----------------- |
| Event Name | FString  | Custom event name |

20 parameter names may be associated with any event. Use "Record Event With Attributes".

![](/files/-LnlSiAU0NsgTfXN1xnV)

**Code**

```
FAnalytics::Get().GetDefaultConfiguredProvider()->RecordEvent(const FString& EventName,
                                                 const TArray<FAnalyticsEventAttribute>& Attributes);
```

{% endtab %}
{% endtabs %}

## Progression event

This event is for games only.

First of all, a Progression event is used for games with short (within one game session) locations/game levels. The event allows you to gather data on passing the locations and get statistics on parameters that vary during the location passing.

Developer must use the following two methods:

{% tabs %}
{% tab title="iOS" %}

1. Method startProgressionEvent when entering the location:

   ```objectivec
   /**
   * The method have to be used when entering the location.
   * @param String locationName - the name of location user entered.
   * @param LocationEventParams params - instance of location parameters class
   */
   [DevToDev startProgressionEvent: locationName withParameters: params];
   ```
2. Method endProgressionEvent when exiting (no matter if completed or not) the location:

   ```objectivec
   /**
   * The method have to be used when the location passing is over.
   * @param String locationName - the name of location user left.
   * @param LocationEventParams params - instance of location parameters class
   */
   [DevToDev endProgressionEvent: locationName withParameters: params];
   ```

   LocationEventParams class methods:

   ```objectivec
     /**
     * Location level of difficulty (optional).
     * @param NSInteger difficultyLevel - level of difficulty
     */
     [params setDifficulty: difficultyLevel];

     /**
     * Previously visited location (optional).
     * @param NSString* locationName -  previously visited location name
     */
     [params setSource: locationName];

     /**
     * State/result of the location passing (required).
     * @param BOOL isCompleted -  true if location is successfuly passed
     */
     [params setIsSuccess: isCompleted];

     /**
     * Time spent in the location (optional).
     * In case the parameter is not specified by the developer, it will be automatically calculated
     * as the date difference between startProgressionEvent and endProgressionEvent method calls.
     * @param NSNumber* duration - time in seconds
     */
     [params setDuration: duration];

     /**
     * User spendings  within the location passing (optional).
     * @ param NSDictionary* spent - user spendings. Key length max. 24 symbols.
     */
     [params setSpent: spent];

     /**
     * User earnings  within the location passing (optional).
     * @param NSDictionary* earned - user earnings. Key length max. 24 symbols.
     */
     [params setEarned: earned];
   ```

{% hint style="warning" %}
The user can be only in one location at the same time. When moving to another location (including embedded), the previous location must be completed. Information on locations, the passing of which was not completed by calling endProgressionEvent method during the game session (the call of endProgressionEvent method is not integrated; user unloaded the application from the device memory; there was an application crash), do not fall in the statistics.&#x20;
{% endhint %}

Let’s look at the example of event integration for a match3 game with a location map:

```objectivec
// Player comes to the third location on the map "Village" while following the game map. 
// Create a parameters object
LocationEventParams* params = [[LocationEventParams alloc] init];

// Specify the known location parameters:
// Passing on the first level of difficulty.
[params setDifficulty:1];

// Before entering this location gamer passed the third location on the map â&#128;&#156;Villageâ&#128;&#157; (optional).
[params setSource: @"Vilage step 02"];

//The location passing starts (required).
[DevToDev startProgressionEvent:@"Vilage step 03" withParameters:params];

// ... Player passing the location.

// Player finishes passing of the third location on the map â&#128;&#156;Villageâ&#128;&#157;

// The location is passed successfully (required).
[params setIsSuccess: YES];

// The passing took 189 seconds.
[params setDuration:@189];

// Location is passed for 54 turns. While the passing gamer used boost and bought extra 5 turns.
NSDictionary* spent = @{
    @"Turns" : @54,
    @"Boost Bomb" : @1,
    @"Extra 5 Turns" : @1
};
[params setSpent: spent];

// Gamer finished the passing with 3 stars and gained 5 coins and 1200 score.
NSDictionary* earned = @{
    @"Stars" : @3,
    @"Score" : @1200,
    @"Coins" : @5
};
[params setEarned: earned]; 

// The location passing is over (required).
[DevToDev endProgressionEvent: @"Vilage step 03" withParameters: params];
```

{% endtab %}

{% tab title="Android" %}

1. Method startProgressionEvent when enetring the location

   ```java
   /**
   * The method have to be used when entering the location.
   * @param String locationName - the name of location user entered.
   * @param LocationEventParams params - instance of location parameters class
   */
   DevToDev.startProgressionEvent(locationName, params);
   ```
2. Method endProgressionEvent when exiting (no matter if completed or not) the location

   ```java
   /**
   * The method have to be used when the location passing is over.
   * @param String locationName - the name of location user left.
   * @param LocationEventParams params - instance of location parameters class
   */
   DevToDev.endProgressionEvent(locationName, params);
   ```

   LocationEventParams class methods:

   ```java
   /**
     * Location level of difficulty (optional).
     * @param int difficultyLevel - level of difficulty
     */
     setDifficulty(difficultyLevel);

     /**
     * Previously visited location (optional).
     * @param String locationName -  previously visited location name
     */
     setSource(locationName);

     /**
     * State/result of the location passing (required).
     * @param boolean isCompleted -  true if location is successfuly passed
     */
     setSuccessfulCompletion(isCompleted);

     /**
     * Time spent in the location (optional).
     * In case the parameter is not specified by the developer, it will be automatically calculated
     * as the date difference between startProgressionEvent and endProgressionEvent method calls.
     * @param int duration - time in seconds
     */
     setDuration(duration);

     /**
     * User spendings within the location passing (optional).
     * @ param HashMap<String, Number> spent - user spendings. Key length max. 24 symbols.
     */
     setSpent(spent);

     /**
     * User earnings within the location passing (optional).
     * @param HashMap<String, Number> earned - user earnings. Key length max. 24 symbols.
     */
     setEarned(earned);
   ```

{% hint style="warning" %}
The user can be only in one location at the same time. When moving to another location (including embedded), the previous location must be completed. Information on locations, the passing of which was not completed by calling endProgressionEvent method during the game session (the call of endProgressionEvent method is not integrated; user unloaded the application from the device memory; there was an application crash) do not fall in the statistics.&#x20;
{% endhint %}

Let’s analyse the example of event integration on match3 game with location map:

```java
// Player comes to the third location on the map "Village" while following the game map. 
// Create a parameters object
LocationEventParams params = new LocationEventParams();

// Specify the known location parameters:
// Passing on the first level of difficulty.
params.setDifficulty(1);

// Before entering this location gamer passed the third location on the map “Village” (optional).
params.setSource("Vilage step 02");

//The location passing starts (required).
DevToDev.startProgressionEvent("Vilage step 03", params);

// ... Player passing the location.

// Player finishes passing of the third location on the map “Village”

// The location is passed successfully (required).
params.setSuccessfulCompletion(true);

// The passing took 189 seconds.
params.setDuration(189);

// Location is passed for 54 turns. While the passing gamer used boost and bought extra 5 turns.
HashMap<String, Number> spent = new HashMap<String, Number>();
spent.put("Turns", 54);
spent.put("Boost Bomb", 1);
spent.put("Extra 5 Turns", 1);
params.setSpent(spent);

// Gamer finished the passing with 3 stars and gained 5 coins and 1200 score.
HashMap<String, Number> earned = new HashMap<String, Number>();
earned.put("Stars", 3);
earned.put("Score", 1200 );
earned.put("Coins", 5);
params.setEarned(earned); 

// The location passing is over (required).
DevToDev.endProgressionEvent("Vilage step 03", params);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

1. Method StartProgressionEvent when enetring the location

   ```csharp
   /**
   * <param name="locationName">The name of location user entered</param>
   * <param name="locationParams">Instance of location parameters class</param>
   */
   DevToDev.SDK.StartProgressionEvent(string locationName, LocationEventParams locationParams);
   ```
2. Method EndProgressionEvent when exiting (no matter if completed or not) the location

   ```csharp
   /**
   * <param name="locationName">The name of location user entered</param>
   * <param name="locationParams">Instance of location parameters class</param>
   */
   DevToDev.SDK.EndProgressionEvent(string locationName, LocationEventParams locationParams);
   ```

   LocationEventParams class methods:

   ```csharp
     /**
     * Location level of difficulty (optional).
     * <param name="difficultyLevel">Level of difficulty</param>
     */
     SetDifficulty(int difficultyLevel);

     /**
     * Previously visited location (optional).
     * <param name="locationName">Previously visited location name</param>
     */
     SetSource(string locationName);

     /**
     * State/result of the location passing (required).
     * <param name="isCompleted">True if location is successfuly passed</param>
     */
     SetSuccessfulCompletion(bool isCompleted);

     /**
     * Time spent in the location (optional).
     * In case the parameter is not specified by the developer, it will be automatically calculated
     * as the date difference between StartProgressionEvent and EndProgressionEvent method calls.
     * <param name="duration">Time in seconds</param>
     */
     SetDuration(long duration);

     /**
     * User spendings within the location passing (optional). Key (currency name) length max. 24 symbols.
     * <param name="spent">User spendings</param>
     */
     SetSpent(Dictionary<string, int> spent);

     /**
     * User earnings within the location passing (optional). Key (currency name) length max. 24 symbols.
     * <param name="earned">User earnings</param>
     */
     SetEarned(Dictionary<string, int> earned);
   ```

{% hint style="warning" %}
The user can be only in one location at the same time. When moving to another location (including embedded), the previous location must be completed. Information on locations, the passing of which was not completed by calling EndProgressionEvent method during the game session (the call of EndProgressionEvent method is not integrated; user unloaded the application from the device memory; there was an application crash) do not fall in the statistics.&#x20;
{% endhint %}

Let’s analyse the example of event integration on match3 game with location map:

```csharp
// Player comes to the third location on the map "Village" while following the game map. 
// Create a parameters object
LocationEventParams locationParams = new LocationEventParams();

// Specify the known location parameters:
// Passing on the first level of difficulty.
locationParams.SetDifficulty(1);

// Before entering this location gamer passed the third location on the map “Village” (optional).
locationParams.SetSource("Vilage step 02");

//The location passing starts (required).
DevToDev.SDK.StartProgressionEvent("Vilage step 03", locationParams);

// ... Player passing the location.

// Player finishes passing of the third location on the map “Village”
LocationEventParams locationParams = new LocationEventParams();

// The location is passed successfully (required).
locationParams.SetSuccessfulCompletion(true);

// The passing took 189 seconds.
locationParams.SetDuration(189);

// Location is passed for 54 turns. While the passing gamer used boost and bought extra 5 turns.
Dictionary<string, int> spent = new Dictionary<string, int>();
spent["Turns"] = 54;
spent["Boost Bomb"] = 1;
spent["Extra 5 Turns"] = 1;
locationParams.SetSpent(spent);

// Gamer finished the passing with 3 stars and gained 5 coins and 1200 score.
Dictionary<string, int> earned = new Dictionary<string, int>();
earned["Stars"] = 3;
earned["Score"] = 1200;
earned["Coins"] = 5;
locationParams.SetEarned(earned); 

// The location passing is over (required).
DevToDev.SDK.EndProgressionEvent("Vilage step 03", locationParams);
```

{% endtab %}

{% tab title="Web" %}

1. Method startProgressionEvent when enetring the location

   ```javascript
   /**
   * The method have to be used when entering the location.
   * @param {string} locationName - the name of location user entered.
   * @param {Object} startParams - location parameters object
   */
   devtodev.startProgressionEvent(locationName, startParams);
   ```
2. Method endProgressionEvent when exiting (no matter if completed or not) the location

   ```javascript
   /**
   * The method have to be used when the location passing is over.
   * @param {string} locationName - the name of location user left.
   * @param {Object} endParams - location parameters object
   */
   devtodev.endProgressionEvent(locationName, endParams);
   ```

   Location parameters object contains:

   ```javascript
     var params = {
         // Previously visited location (optional).
         "source" : "locationSource",

         // Location level of difficulty (optional).
         "difficulty" : 1,

         // Time spent in the location (optional).
         // In case the parameter is not specified by the developer, it will be automatically calculated
         // as the date difference between startProgressionEvent and endProgressionEvent method calls.
         "duration" : 80,

         // State/result of the location passing (required).
         "success" : true,

         //User spendings within the location passing (optional).
         "spent" : [
             {
                 "currency": "currency 1 name", // Currency name length max. 24 symbols.
                 "amount": 1   // objects with amount value less than 1 are ignored
             },
             {
                 "currency": "currency 2 name",
                 "amount": 2
             }
         ],

         // User earnings within the location passing (optional).
         "earned" : [
             {
                 "currency": "currency 1 name",
                 "amount": 1 // objects with amount value less than 1 are ignored
             },
             {
                 "currency": "currency 2 name",
                 "amount": 2
             }
         ]
     };
   ```

{% hint style="warning" %}
The user can be only in one location at the same time. When moving to another location (including embedded), the previous location must be completed. Information on locations, the passing of which was not completed by calling endProgressionEvent method during the game session (the call of endProgressionEvent method is not integrated; user unloaded the application from the device memory; there was an application crash) do not fall in the statistics.&#x20;
{% endhint %}

Let’s analyse the example of event integration on match3 game with location map:

```javascript
// Player comes to the third location on the map "Village" while following the game map. 
// Create a parameters object
var params = {};

// Specify the known location parameters:
// Passing on the first level of difficulty.
params["difficulty"] = 1;

// Before entering this location gamer passed the third location on the map “Village” (optional).
params["source"] = "Vilage step 02";

//The location passing starts (required).
devtodev.startProgressionEvent("Vilage step 03", params);

// ... Player passing the location.

// Player finishes passing of the third location on the map “Village”

// The location is passed successfully (required).
params["success"] = true;

// The passing took 189 seconds.
params["duration"] = 189;

// Location is passed for 54 turns. While the passing gamer used boost and bought extra 5 turns.
params["spent"] = [
    {
        "currency": "Turns",
        "amount": 54
    },
    {
        "currency": "Boost Bomb",
        "amount": 1
    },
    {
        "currency": "Extra 5 Turns",
        "amount": 1
    }
];

// Gamer finished the passing with 3 stars and gained 5 coins and 1200 score.
params["earned"] = [
    {
        "currency": "Stars",
        "amount": 3
    },
    {
        "currency": "Score",
        "amount": 1200
    },
    {
        "currency": "Coins",
        "amount": 5
    }
];

// The location passing is over (required).
devtodev.endProgressionEvent("Vilage step 03", params);
```

{% endtab %}

{% tab title="Unity" %}

1. Method StartProgressionEvent when enetring the location

   ```csharp
   /// <param name="eventId"> The name of location user entered </param>
   /// <param name="eventParams"> Instance of progression parameters class </param>
   DevToDev.Analytics.StartProgressionEvent(string eventId, ProgressionEventParams eventParams);
   ```
2. Method EndProgressionEvent when exiting (no matter if completed or not) the location

   ```csharp
   /// <param name="eventId"> The name of location user left </param>
   /// <param name="eventParams"> Instance of progression parameters class </param>
   DevToDev.Analytics.EndProgressionEvent(string eventId, ProgressionEventParams eventParams);
   ```

   ProgressionEventParams class methods:

   ```csharp
     /// <summary> Location level of difficulty (optional). </summary>
     /// <param name="difficultyLevel"> Level of difficulty </param>
     SetDifficulty(int difficultyLevel);

     /// <summary> Previously visited location (optional). </summary>
     /// <param name="locationName"> Previously visited location name </param>
     SetSource(string locationName);

     /// <summary> State/result of the location passing (required). </summary>
     /// <param name="isCompleted"> True if location is successfuly passed </param>
     SetSuccessfulCompletion(bool isCompleted);

     /// <summary>Time spent in the location (optional).
     /// <para>In case the parameter is not specified by the developer, it will be automatically calculated
     /// as the date difference between StartProgressionEvent and EndProgressionEvent method calls.</para>
     /// </summary>
     /// <param name="duration"> Time in seconds </param>
     SetDuration(long duration);

     /// <summary> User spendings within the location passing (optional). Dictionary key max.length is 24 symbols.</summary>
     /// <param name="spent"> User spendings </param>
     SetSpent(Dictionary<string, int> spent);

     /// <summary> User earnings within the location passing (optional). </summary>
     /// <param name="earned"> User earnings </param>
     SetEarned(Dictionary<string, int> earned);
   ```

{% hint style="warning" %}
The user can be only in one location at the same time. When moving to another location (including embedded), the previous location must be completed. Information on locations, the passing of which was not completed by calling EndProgressionEvent method during the game session (the call of EndProgressionEvent method is not integrated; user unloaded the application from the device memory; there was an application crash) do not fall in the statistics.&#x20;
{% endhint %}

Let’s analyse the example of event integration on match3 game with location map:

```csharp
// Player comes to the third location on the map "Village" while following the game map. 
// Create a parameters object
ProgressionEventParams locationParams = new ProgressionEventParams();

// Specify the known location parameters:
// Passing on the first level of difficulty.
locationParams.SetDifficulty(1);

// Before entering this location gamer passed the third location on the map “Village” (optional).
locationParams.SetSource("Vilage step 02");

//The location passing starts (required).
DevToDev.Analytics.StartProgressionEvent("Vilage step 03", locationParams);

// ... Player passing the location.

// Player finishes passing of the third location on the map “Village”
// Create a parameters object
ProgressionEventParams locationParams = new ProgressionEventParams();

// The location is passed successfully (required).
locationParams.SetSuccessfulCompletion(true);

// The passing took 189 seconds.
locationParams.SetDuration(189);

// Location is passed for 54 turns. While the passing gamer used boost and bought extra 5 turns.
Dictionary<string, int> spent = new Dictionary<string, int>();
spent["Turns"] = 54;
spent["Boost Bomb"] = 1;
spent["Extra 5 Turns"] = 1;
locationParams.SetSpent(spent);

// Gamer finished the passing with 3 stars and gained 5 coins and 1200 score.
Dictionary<string, int> earned = new Dictionary<string, int>();
earned["Stars"] = 3;
earned["Score"] = 1200;
earned["Coins"] = 5;
locationParams.SetEarned(earned); 

// The location passing is over (required).
DevToDev.Analytics.EndProgressionEvent("Vilage step 03", locationParams);
```

{% endtab %}

{% tab title="Mac OS" %}

1. Method startProgressionEvent when enetring the location

   ```objectivec
   /**
   * The method have to be used when entering the location.
   * @param String locationName - the name of location user entered.
   * @param LocationEventParams params - instance of location parameters class
   */
   [DevToDev startProgressionEvent: locationName withParameters: params];
   ```
2. Method endProgressionEvent when exiting (no matter if completed or not) the location

   ```objectivec
   /**
   * The method have to be used when the location passing is over.
   * @param String locationName - the name of location user left.
   * @param LocationEventParams params - instance of location parameters class
   */
   [DevToDev endProgressionEvent: locationName withParameters: params];
   ```

   LocationEventParams class methods:

   ```objectivec
   /**
     * Location level of difficulty (optional).
     * @param NSInteger difficultyLevel - level of difficulty
     */
     [params setDifficulty: difficultyLevel];

     /**
     * Previously visited location (optional).
     * @param NSString* locationName -  previously visited location name
     */
     [params setSource: locationName];

     /**
     * State/result of the location passing (required).
     * @param BOOL isCompleted -  true if location is successfuly passed
     */
     [params setIsSuccess: isCompleted];

     /**
     * Time spent in the location (optional).
     * In case the parameter is not specified by the developer, it will be automatically calculated
     * as the date difference between startProgressionEvent and endProgressionEvent method calls.
     * @param NSNumber* duration - time in seconds
     */
     [params setDuration: duration];

     /**
     * User spendings  within the location passing (optional).
     * @ param NSDictionary* spent - user spendings. Key max.length is 24 symbols.
     */
     [params setSpent: spent];

     /**
     * User earnings  within the location passing (optional).
     * @param NSDictionary* earned - user earnings.  Key max.length is 24 symbols.
     */
     [params setEarned: earned];
   ```

{% hint style="warning" %}
The user can be only in one location at the same time. When moving to another location (including embedded), the previous location must be completed. Information on locations, the passing of which was not completed by calling endProgressionEvent method during the game session (the call of endProgressionEvent method is not integrated; user unloaded the application from the device memory; there was an application crash) do not fall in the statistics.&#x20;
{% endhint %}

Let’s analyse the example of event integration on match3 game with location map:

```objectivec
// Player comes to the third location on the map "Village" while following the game map. 
// Create a parameters object
LocationEventParams* params = [[LocationEventParams alloc] init];

// Specify the known location parameters:
// Passing on the first level of difficulty.
[params setDifficulty:1];

// Before entering this location gamer passed the third location on the map “Village” (optional).
[params setSource: @"Vilage step 02"];

//The location passing starts (required).
[DevToDev startProgressionEvent:@"Vilage step 03" withParameters:params];

// ... Player passing the location.

// Player finishes passing of the third location on the map “Village”

// The location is passed successfully (required).
[params setIsSuccess: YES];

// The passing took 189 seconds.
[params setDuration:@189];

// Location is passed for 54 turns. While the passing gamer used boost and bought extra 5 turns.
NSDictionary* spent = @{
    @"Turns" : @54,
    @"Boost Bomb" : @1,
    @"Extra 5 Turns" : @1
};
[params setSpent: spent];

// Gamer finished the passing with 3 stars and gained 5 coins and 1200 score.
NSDictionary* earned = @{
    @"Stars" : @3,
    @"Score" : @1200,
    @"Coins" : @5
};
[params setEarned: earned]; 

// The location passing is over (required).
[DevToDev endProgressionEvent: @"Vilage step 03" withParameters: params];
```

{% endtab %}

{% tab title="Adobe Air" %}

1. Method *StartProgressionEvent* when enetring the location

   ```javascript
   /**
   * The method have to be used when entering the location.
   * @param locationName - the name of location user entered.
   * @param params - instance of location parameters class
   */
   DevToDev.StartProgressionEvent(locationName:String, params:LocationEventParams);
   ```
2. Method *EndProgressionEvent* when exiting (no matter if completed or not) the location

   ```javascript
   /**
   * The method have to be used when the location passing is over.
   * @param locationName - the name of location user left.
   * @param params - instance of location parameters class
   */
   DevToDev.EndProgressionEvent(locationName:String, params:LocationEventParams);
   ```

   *LocationEventParams* class methods:

   ```javascript
     /**
     * Location level of difficulty (optional).
     * @param difficultyLevel - level of difficulty
     */
     SetDifficulty(difficultyLevel:int);

     /**
     * Previously visited location (optional).
     * @param locationName -  previously visited location name
     */
     SetSource(locationName:String);

     /**
     * State/result of the location passing (required).
     * @param isCompleted -  true if location is successfuly passed
     */
     SetSuccessfulCompletion(isCompleted:Boolean);

     /**
     * Time spent in the location (optional).
     * In case the parameter is not specified by the developer, it will be automatically calculated
     * as the date difference between StartProgressionEvent and EndProgressionEvent method calls.
     * @param duration - time in seconds
     */
     SetDuration(duration:int);

     /**
     * User spendings within the location passing (optional).
     * @ param spent - user spendings. Key max.length is 24 symbols.
     */
     SetSpent(spent:Dictionary);

     /**
     * User earnings within the location passing (optional).
     * @param earned - user earnings. Key max.length is 24 symbols.
     */
     SetEarned(earned:Dictionary);
   ```

{% hint style="warning" %}
The user can be only in one location at the same time. When moving to another location (including embedded), the previous location must be completed. Information on locations, the passing of which was not completed by calling EndProgressionEvent method during the game session (the call of EndProgressionEvent method is not integrated; user unloaded the application from the device memory; there was an application crash) do not fall in the statistics.&#x20;
{% endhint %}

Let’s analyse the example of event integration on match3 game with location map:

```javascript
// Player comes to the third location on the map "Village" while following the game map. 
// Create a parameters object
var params:LocationEventParams = new LocationEventParams();

// Specify the known location parameters:
// Passing on the first level of difficulty.
params.SetDifficulty(1);

// Before entering this location gamer passed the third location on the map “Village” (optional).
params.SetSource("Vilage step 02");

//The location passing starts (required).
DevToDev.StartProgressionEvent("Vilage step 03", params);

// ... Player passing the location.

// Player finishes passing of the third location on the map “Village”
var params:LocationEventParams = new LocationEventParams();

// The location is passed successfully (required).
params.SetSuccessfulCompletion(true);

// The passing took 189 seconds.
params.SetDuration(189);

// Location is passed for 54 turns. While the passing gamer used boost and bought extra 5 turns.
var spent:Dictionary = new Dictionary();
spent["Turns"] = 54;
spent["Boost Bomb"] = 1;
spent["Extra 5 Turns"] = 1;
params.SetSpent(spent);

// Gamer finished the passing with 3 stars and gained 5 coins and 1200 score.
var earned:Dictionary = new Dictionary();
earned["Stars"] = 3;
earned["Score"] = 1200;
earned["Coins"] = 5;
params.SetEarned(earned); 

// The location passing is over (required).
DevToDev.EndProgressionEvent("Vilage step 03", params);
```

{% endtab %}

{% tab title="UE4" %}

1. Method StartProgressionEvent when enetring the location

   **Blueprint**\
   ![](/files/-LnlSiCcCcs5hJ2SNgbP)

   | Field        | Type                         | Description                       |
   | ------------ | ---------------------------- | --------------------------------- |
   | locationName | FString                      | The name of location user entered |
   | Attributes   | TArray\<FAnalyticsEventAttr> | Location parameters               |

   **Code**

   ```cpp
   // The method have to be used when entering the location.
   // FString locationName  - the name of location user entered.
   // TArray<FAnalyticsEventAttr> Attributes - location parameters
   UDevToDevBlueprintFunctionLibrary::StartProgressionEvent(const FString& locationName,
                                                            const TArray<FAnalyticsEventAttr>& Attributes);
   ```
2. Method EndProgressionEvent when exiting (no matter if completed or not) the location**Blueprint**

   ![](/files/-LnlSiChfoNzrTZU-LWT)

   | Field        | Type                         | Description                                                                           |
   | ------------ | ---------------------------- | ------------------------------------------------------------------------------------- |
   | locationName | FString                      | The name of location user left                                                        |
   | Attributes   | TArray\<FAnalyticsEventAttr> | Location parameters                                                                   |
   | Earned       | TArray\<FAnalyticsEventAttr> | User earnings within the location passing (optional). Key max. length is 24 symbols.  |
   | Spent        | TArray\<FAnalyticsEventAttr> | User spendings within the location passing (optional). Key max. length is 24 symbols. |

   Location parameters

   | Key        | Type    | Description                                                                                                                                                                                                                        |
   | ---------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
   | success    | bool    | State/result of the location passing (required).                                                                                                                                                                                   |
   | source     | FString | Previously visited location (optional).                                                                                                                                                                                            |
   | difficulty | int32   | Location level of difficulty (optional).                                                                                                                                                                                           |
   | duration   | int32   | Time spent in the location (optional). In case the parameter is not specified by the developer, it will be automatically сalculated as the date difference between Start Progression Event and End Progression Event method calls. |

   **Code**

   ```cpp
   // The method have to be used when the location passing is over.
   // FString locationName  - the name of location user left.
   // TArray<FAnalyticsEventAttr> Attributes - location parameters
   // TArray<FAnalyticsEventAttr> Earned - user earnings within the location passing (optional)
   // TArray<FAnalyticsEventAttr> Spent - user spendings within the location passing (optional).
   UDevToDevBlueprintFunctionLibrary::EndProgressionEvent(const FString& locationName,
                                                          const TArray<FAnalyticsEventAttr>& Attributes,
                                                          const TArray<FAnalyticsEventAttr>& Earned,
                                                          const TArray<FAnalyticsEventAttr>& Spent);
   ```

{% hint style="warning" %}
The user can be only in one location at the same time. When moving to another location (including embedded), the previous location must be completed. Information on locations, the passing of which was not completed by calling endProgressionEvent method during the game session (the call of endProgressionEvent method is not integrated; user unloaded the application from the device memory; there was an application crash) do not fall in the statistics.&#x20;
{% endhint %}

**Let’s analyse the example of event integration on match3 game with location map:**

Player comes to the third location on the map “Village” while following the game map. Passing on the first level of difficulty. Before entering this location gamer passed the third location on the map “City”. *.. Player passing the location.* Player finishes passing of the third location on the map “Village”. The location is passed successfully. The passing took 389 seconds. Gamer finished the passing with 3 stars and gained 70 coins. While the passing gamer used boost and bought extra 5 turns.![](https://blobscdn.gitbook.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LnBolWcwPO33iga2rY2%2F-LnTASe7tY_8XGfgeRsv%2F-LnTAVQpI5nBbFhOLA7I%2F142-basicmethods-10.png?generation=1567095661652472\&alt=media)[  <br>](https://took.gitbook.io/1234/integration/ue4/analytics-integration)
{% endtab %}
{% endtabs %}


# Secondary methods

{% hint style="danger" %}
**This generation of SDK is deprecated and is no longer supported.**\
Information about the [current version can be found here](/integration/integration-of-sdk-v2/setting-up-events/secondary-methods).
{% endhint %}

## Initial referrer tracking

{% tabs %}
{% tab title="iOS" %}
Unfortunately, Apple does not provide any capability to pass a referrer string through to your app from a link to the App Store. But if you have a referral info, you can set it using the method below:

```objectivec
/**
 * Tracks user's referral data
 * ### Usage:
 *    [DevToDev referrer:@{
 *       RFSource: @"adwords",
 *       RFMedium: @"cpi",
 *       RFContent: @"Snow Boots",
 *       RFCampaign: @"Warm Snow Boots",
 *       RFTerm: @"snow boots"
 *    }];
 * 
 * @param NSDictionary<ReferralProperty*, NSString*> utm - Dictionary with referrer values
 */
[DevToDev referrer: (NSDictionary<ReferralProperty*, NSString*> *) utm];
```

The list of predefined keys:

```objectivec
//To identify a search engine, newsletter name, or other source.
// (for example 'AdWords', 'Bing', 'E-Mail Newsletter')
ReferralProperty * RFSource;

//To identify a medium such as email or cost-per-install.
// (for example 'CPI')
ReferralProperty * RFMedium;

//To identify a specific product promotion or strategic campaign.
//(for example 'Snow Boots')
ReferralProperty * RFCampaign;

//To differentiate ads or links that point to the same URL.
//(for example some ads might advertise 'Warm Snow Boots' and others might advertise 'Durable Snow Boots')
ReferralProperty * RFContent;

//To note the keywords for this ad.
// for example 'shoes+boots')
ReferralProperty * RFTerm;

//To add a custom key
[ReferralProperty Custom:@"your_key_name"];
```

{% endtab %}

{% tab title="Android" %}

{% endtab %}

{% tab title="Windows 8.1 and 10" %}
Unfortunately, Windows Store does not provide any capability to pass a referrer string through to your app from a link to the store. But if you have a referral info, you can set it using the method below:

```csharp
/**
 * ### Usage:
 *     Dictionary<ReferralProperty, string> referralData = new Dictionary<ReferralProperty, string>();
 *     referralData.Add(ReferralProperty.Source, "source");
 *     referralData.Add(ReferralProperty.Medium, "medium");
 *     referralData.Add(ReferralProperty.Content, "content");
 *     referralData.Add(ReferralProperty.Campaign, "campaign");
 *     referralData.Add(ReferralProperty.Term, "term");
 *     referralData.Add(ReferralProperty.Custom("site"), "site");
 *     DevToDev.SDK.Referral(referralData);
 *
 * <param name="referralData">Dictionary with referrer values</param>
 */
DevToDev.SDK.Referral(Dictionary<ReferralProperty, string> referralData);
```

The list of predefined keys:

```csharp
//To identify a search engine, newsletter name, or other source.
// (for example 'AdWords', 'Bing', 'E-Mail Newsletter')
ReferralProperty.Source;

//To identify a medium such as email or cost-per-install.
// (for example 'CPI')
ReferralProperty.Medium;

//To identify a specific product promotion or strategic campaign.
//(for example 'Snow Boots')
ReferralProperty.Campaign;

//To differentiate ads or links that point to the same URL.
//(for example some ads might advertise 'Warm Snow Boots' and others might advertise 'Durable Snow Boots')
ReferralProperty.Content;

//To note the keywords for this ad.
// for example 'shoes+boots')
ReferralProperty.Term;

//To add a custom key
ReferralProperty.Custom("your_key_name");
```

{% endtab %}

{% tab title="Web" %}

{% endtab %}

{% tab title="Unity" %}
Automated referral parameters are available on Android platform. Unfortunately, other platforms do not provide any capability to pass a referrer string through to your app from a link to the store. But if you have a referral info, you can set it using the method below:

```csharp
/// <summary> Initial referrer tracking <summary>
/// <example> Usage:
/// 
///     Dictionary<ReferralProperty, string> referralData = new Dictionary<ReferralProperty, string>();
///     referralData.Add(ReferralProperty.Source, "source");
///     referralData.Add(ReferralProperty.Medium, "medium");
///     referralData.Add(ReferralProperty.Content, "content");
///     referralData.Add(ReferralProperty.Campaign, "campaign");
///     referralData.Add(ReferralProperty.Term, "term");
///     referralData.Add(ReferralProperty.Custom("site"), "site");
///     DevToDev.Analytics.Referral(referralData);
/// 
/// </example>
/// <param name="referralData"> Dictionary with referrer values </param>
DevToDev.Analytics.Referral(Dictionary<ReferralProperty, string> referralData);
```

The list of predefined keys:

```csharp
// To identify a search engine, newsletter name, or other source.
// (for example 'AdWords', 'Bing', 'E-Mail Newsletter')
ReferralProperty.Source;

// To identify a medium such as email or cost-per-install.
// (for example 'CPI')
ReferralProperty.Medium;

// To identify a specific product promotion or strategic campaign.
// (for example 'Snow Boots')
ReferralProperty.Campaign;

// To differentiate ads or links that point to the same URL.
//(for example some ads might advertise 'Warm Snow Boots' and others might advertise 'Durable Snow Boots')
ReferralProperty.Content;

// To note the keywords for this ad.
// (for example 'shoes+boots')
ReferralProperty.Term;

// To add a custom key
ReferralProperty.Custom("your_key_name");
```

{% endtab %}

{% tab title="Mac OS" %}
Unfortunately, Apple does not provide any capability to pass a referrer string through to your app from a link to the app store. But if you have a referral info, you can set it using the method below:

```objectivec
/**
 * Tracks user's referral data
 * ### Usage:
 *    [DevToDev referrer:@{
 *       RFSource: @"adwords",
 *       RFMedium: @"cpi",
 *       RFContent: @"Snow Boots",
 *       RFCampaign: @"Warm Snow Boots",
 *       RFTerm: @"snow boots"
 *    }];
 * 
 * @param NSDictionary<ReferralProperty*, NSString*> utm - Dictionary with referrer values
 */
[DevToDev referrer: (NSDictionary<ReferralProperty*, NSString*> *) utm];
```

The list of predefined keys:

```objectivec
//To identify a search engine, newsletter name, or other source.
// (for example 'AdWords', 'Bing', 'E-Mail Newsletter')
ReferralProperty * RFSource;

//To identify a medium such as email or cost-per-install.
// (for example 'CPI')
ReferralProperty * RFMedium;

//To identify a specific product promotion or strategic campaign.
//(for example 'Snow Boots')
ReferralProperty * RFCampaign;

//To differentiate ads or links that point to the same URL.
//(for example some ads might advertise 'Warm Snow Boots' and others might advertise 'Durable Snow Boots')
ReferralProperty * RFContent;

//To note the keywords for this ad.
// for example 'shoes+boots')
ReferralProperty * RFTerm;

//To add a custom key
[ReferralProperty Custom:@"your_key_name"];
```

{% endtab %}

{% tab title="Adobe Air" %}
Automated referral parameters is available on Android platform. Unfortunately, other platforms do not provide any capability to pass a referrer string through to your app from a link to the store. But if you have a referral info, you can set it using the method below:

```javascript
/**
* ### Usage:
*     var referralData:Dictionary = new Dictionary();
*     referralData[ReferralProperty.Source] = "source";
*     referralData[ReferralProperty.Medium] = "medium";
*     referralData[ReferralProperty.Content] = "content";
*     referralData[ReferralProperty.Campaign] = "campaign";
*     referralData[ReferralProperty.Term] = "term";
*     referralData[ReferralProperty.Custom("site")] = "site";
*     DevToDev.referral(referralData);
*
* @ param referralData - dictionary with referrer values
*/
DevToDev.referral(referralData:Dictionary);
```

```
The list of predefined keys:
```

```javascript
// To identify a search engine, newsletter name, or other source.
// (for example 'AdWords', 'Bing', 'E-Mail Newsletter')
ReferralProperty.Source;

// To identify a medium such as email or cost-per-install.
// (for example 'CPI')
ReferralProperty.Medium;

// To identify a specific product promotion or strategic campaign.
// (for example 'Snow Boots')
ReferralProperty.Campaign;

// To differentiate ads or links that point to the same URL.
// (for example some ads might advertise 'Warm Snow Boots' and others might advertise 'Durable Snow Boots')
ReferralProperty.Content;

// To note the keywords for this ad.
// (for example 'shoes+boots')
ReferralProperty.Term;

// To add a custom key
ReferralProperty.Custom("your_key_name");
```

{% endtab %}

{% tab title="UE4" %}
Unfortunately, Apple does not provide any capability to pass a referrer string through to your app from a link to the app store. But if you have a referral info, you can set it using the method below:

**Blueprint**

![](https://blobscdn.gitbook.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LnBolWcwPO33iga2rY2%2F-LnTASe7tY_8XGfgeRsv%2F-LnTAUvoKzKCb7guBr_O%2F143-secondarymethods-0.png?generation=1567095659196400\&alt=media)

| Field    | Type    | Description                                                                                                                                                        |
| -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Source   | FString | To identify a search engine, newsletter name, or other source. (for example 'AdWords', 'Bing', 'E-Mail Newsletter')                                                |
| Medium   | FString | To identify a medium such as email or cost-per-install. (for example 'CPI')                                                                                        |
| Campaign | FString | To identify a specific product promotion or strategic campaign. (for example 'Snow Boots')                                                                         |
| Content  | FString | To differentiate ads or links that point to the same URL. (for example some ads might advertise 'Warm Snow Boots' and others might advertise 'Durable Snow Boots') |
| Term     | FString | To note the keywords for this ad. for example 'shoes+boots')                                                                                                       |

**Code**

```cpp
UDevToDevBlueprintFunctionLibrary::Referrer(const TArray<FAnalyticsEventAttr>& Attributes);
```

{% endtab %}
{% endtabs %}

## **Connecting to social networks**

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* Tracks the existence of a connection with a social network. 
* Use pre-defined or custom values as an identifier.
* @param SocialNetwork socialNetwork - social network id
*/
[DevToDev socialNetworkConnect: (SocialNetwork *) socialNetwork];javascript:void(0)
```

Use the current constants to specify a social network:

```objectivec
Facebook
Twitter
GooglePlus
VK
//and so on...
```

Otherwise, create an object with the social network name you need.

```objectivec
SocialNetwork  socialNetwork = [SocialNetwork Custom: (NSString *) networkName]; (max. 24 symbols)
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* Tracks the existence of a connection with a social network.
* Use pre-defined or custom values as an identifier.
* @param SocialNetwork socialNetwork - social network id
*/
DevToDev.socialNetworkConnect(SocialNetwork socialNetwork);
```

**Use the current constants to specify a social network:**

* SocialNetwork.Facebook
* SocialNetwork.Twitter
* SocialNetwork.GooglePlus
* SocialNetwork.Vk
* and so on...

Otherwise, create your own social network object.

```java
/**
* Custom social network object
* @param networkName - social network name (max. 24 symbols)
*/
SocialNetwork socialNetwork = SocialNetwork.Custom(String networkName);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
* Tracks the existence of a connection with a social network.
* Use pre-defined or custom values as an identifier.
* <param name="socialNetwork"> Social network ID </param>
*/
DevToDev.SDK.SocialNetworkConnect(SocialNetwork socialNetwork);
```

Example:

```csharp
DevToDev.SDK.SocialNetworkConnect(SocialNetwork.Facebook);
```

Use the current constants to specify social network:

SocialNetwork.Facebook\
SocialNetwork.Twitter\
SocialNetwork.GooglePlus\
SocialNetwork.Vk\
and so on...

Otherwise, create social network the object of your own:

```
SocialNetwork socialNetwork = SocialNetwork.Custom(string networkName); //(max. 24 symbols)
```

{% endtab %}

{% tab title="Web" %}

```javascript
/**
* Tracks the existence of a connection with a social network.
* Use pre-defined or custom values as an identifier.
* @param {string} socialNetwork - social network id (max. 24 symbols)
**/

devtodev.socialNetworkConnect(socialNetwork);
```

We recommend using the following values for the most popular social networks:

| Value        | Social Network | Value | Social Network |
| ------------ | -------------- | ----- | -------------- |
| en           | Evernote       | rt    | Reddit         |
| fb           | Facebook       | rr    | Renren         |
| gm           | Google Mail    | tb    | Tumblr         |
| gp           | Google+        | tw    | Twitter        |
| in           | LinkedIn       | vk    | VK             |
| ok           | Odnoklassniki  | vb    | Viber          |
| pi           | Pinterest      | wp    | WhatsApp       |
| qq           | Qzone          |       |                |
| {% endtab %} |                |       |                |

{% tab title="Unity" %}

```csharp
/// <summary> Track the existence of a connection with a social network. 
/// Use pre-defined or custom values as an identifier.</summary>
/// <param name="socialNetwork"> Social network ID </param>
DevToDev.Analytics.SocialNetworkConnect(DevToDev.SocialNetwork socialNetwork);
```

Use the current constants to specify social network:

```csharp
DevToDev.SocialNetwork.Facebook
DevToDev.SocialNetwork.Twitter
DevToDev.SocialNetwork.GooglePlus
DevToDev.SocialNetwork.Vk
// and so on...
```

Otherwise, create your own social network object.

```csharp
DevToDev.SocialNetwork socialNetwork = DevToDev.SocialNetwork.Custom(string networkName); //(max. 24 symbols)
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* Tracks the existence of a connection with a social network. Use pre-defined or custom values as an identifier.
* @param SocialNetwork socialNetwork - social network id
*/
[DevToDev socialNetworkConnect: (SocialNetwork *) socialNetwork];
```

Use the current constants to specify social network:

```objectivec
Facebook
Twitter
GooglePlus
VK
//and so on...
```

Otherwise, create social network the object of your own.

```objectivec
SocialNetwork  socialNetwork = [SocialNetwork Custom: (NSString *) networkName]; //max. 24 symbols
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* Tracks the existence of a connection with a social network.
* Use pre-defined or custom values as an identifier.
* @param socialNetwork - social network id
*/
DevToDev.socialNetworkConnect(socialNetwork:SocialNetwork);
```

Use the current constants to specify social network:

```java
SocialNetwork.VK;
SocialNetwork.TWITTER;
SocialNetwork.FACEBOOK;
SocialNetwork.GOOGLE_PLUS;
SocialNetwork.WHATS_APP;
SocialNetwork.VIBER;
SocialNetwork.EVERNOTE;
SocialNetwork.GOOGLE_MAIL;
SocialNetwork.LINKED_IN;
SocialNetwork.PINTEREST;
SocialNetwork.QZONE;
SocialNetwork.REDDIT;
SocialNetwork.RENREN;
SocialNetwork.TUMBLR;
```

Otherwise, create your own social network object:

```javascript
/**
* Custom social network object
* @param networkName - social network name (max. 24 symbols)
*/
var socialNetwork:SocialNetwork = SocialNetwork.Custom(networkName:String);
```

{% endtab %}

{% tab title="UE4" %}
Blueprint

![](/files/-LnlSiDyqXJunBncBiSC)

| **Field**   | **Type** | **Description**   |
| ----------- | -------- | ----------------- |
| Social Name | FString  | Social network Id |

### Code

```cpp
// Tracks the existence of a connection with a social network.
// Use pre-defined or custom values as an identifier.
// FString socialNetwork - social network id (max. 24 symbols)

UDevToDevBlueprintFunctionLibrary::SocialNetworkConnect(const FString& socialNetwork);
```

We recommend using the following values for the most popular social networks:

| Value         | Social Network | Value | Social Network |
| ------------- | -------------- | ----- | -------------- |
| en            | Evernote       | rt    | Reddit         |
| fb            | Facebook       | rr    | Renren         |
| gm            | Google Mail    | tb    | Tumblr         |
| gp            | Google+        | tw    | Twitter        |
| in            | LinkedIn       | vk    | VK             |
| ok            | Odnoklassniki  | vb    | Viber          |
| pi            | Pinterest      | wp    | WhatsApp       |
| qq            | Qzone          |       |                |
| {% endtab %}  |                |       |                |
| {% endtabs %} |                |       |                |

## **Posting to social networks**

Track publications in social networks and analyze the effectiveness of viral messages. The event is sent after a social network confirms the publication.

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* Tracks the existence of posts to a social network.
* @param socialNetwork - social network Id
* @param NSString reason - the reason of posting (max. 32 symbols)
*/
[DevToDev socialNetworkPost: (SocialNetwork *) socialNetwork withReason: (NSString *) reason];
```

As a 'reason' parameter we recommend you indicate actions which encourage users to make a publication.

| <p>For example:</p><ul><li>Start playing</li><li>New level reached</li><li>New building</li><li>New ability</li><li>Quest completed</li><li>New item</li><li>Collection completed</li><li>Invitation</li></ul> | <ul><li>Asking for help</li><li>New Record</li><li>Achievement</li><li>URL sharing</li><li>Recommendation</li><li>Review</li></ul><p>and so on...</p> |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |

```objectivec
Facebook
Twitter
GooglePlus
VK
//and so on...
```

Otherwise, create an object with the social network name you need.

```objectivec
SocialNetwork  socialNetwork = [SocialNetwork Custom: (NSString *) networkName];
// networkName (max. 24 symbols)
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* Tracks the existence of posts to a social network.
* @param socialNetwork - social network Id
* @param reason - the reason of posting (max. 32 symbols)
*/
DevToDev.socialNetworkPost(SocialNetwork socialNetwork, String reason);
```

As a «reason» parameter we recommend that you indicate actions which encourage users to make a publication.

For example:

* Start playing
* New level reached
* New building
* New ability
* Quest completed
* New item
* Collection completed
* Invitation
* Asking for help
* New Record
* Acheivement
* URL sharing
* Recommendation
* Review
* and so on...

Use the current constants to specify a social network:

* SocialNetwork.Facebook
* SocialNetwork.Twitter
* SocialNetwork.GooglePlus
* SocialNetwork.Vk
* and so on...

Otherwise, create your own social network object.

```java
/**
* Custom social network object
* @param networkName - social network name (max. 24 symbols)
*/
SocialNetwork socialNetwork = SocialNetwork.Custom(String networkName);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}
The social network ID is the same as with DevToDev.SDK.SocialNetworkConnect(). It is possible to use pre-defined or custom values as the reason (pReason parameter) .

```csharp
/**
*  <param name="networkName"> Social network ID </param>
*  <param name="reason"> The reason of posting. (max. 32 symbols)</param>
*/
DevToDev.SDK.SocialNetworkPost(SocialNetwork socialNetwork, String reason)
```

Example:

```csharp
DevToDev.SDK.SocialNetworkPost(SocialNetwork.Facebook, "newLevelReached");
```

As a «reason» parameter we recommend that you indicate actions which encourage users to make publication.

For example:

* Start playing
* New level reached
* New building
* New ability
* Quest completed
* New item
* Collection completed
* Invitation
* Asking for help
* New Record
* Acheivement
* URL sharing
* Recommendation
* Review

and so on...

Use the current constants to specify social network:

SocialNetwork.Facebook\
SocialNetwork.Twitter\
SocialNetwork.GooglePlus\
SocialNetwork.Vk\
and so on...

Otherwise, create social network the object of your own:

```csharp
SocialNetwork socialNetwork = SocialNetwork.Custom(string networkName); //(max. 24 symbols)
```

{% endtab %}

{% tab title="Web" %}

```
/**
* Tracks the existence of posts to a social network.
* @param {string} socialNetwork - social network Id (max. 24 symbols)
* @param {string} reason - the reason of posting (max. 32 symbols)
*/

devtodev.socialNetworkPost(socialNetwork, reason);
```

As a «reason» parameter we recommend that you indicate actions which encourage users to make publication.

| <p>For example:</p><ul><li>Start playing</li><li>New level reached</li><li>New building</li><li>New ability</li><li>Quest completed</li><li>New item</li><li>Collection completed</li><li>Invitation</li></ul> | <ul><li>Asking for help</li><li>New Record</li><li>Achiеvement</li><li>URL sharing</li><li>Recommendation</li><li>Review</li></ul><p>and so on...</p> |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |

| Value        | Social Network | Value | Social Network |
| ------------ | -------------- | ----- | -------------- |
| en           | Evernote       | rt    | Reddit         |
| fb           | Facebook       | rr    | Renren         |
| gm           | Google Mail    | tb    | Tumblr         |
| gp           | Google+        | tw    | Twitter        |
| in           | LinkedIn       | vk    | VK             |
| ok           | Odnoklassniki  | vb    | Viber          |
| pi           | Pinterest      | wp    | WhatsApp       |
| qq           | Qzone          |       |                |
| {% endtab %} |                |       |                |

{% tab title="Unity" %}

```csharp
/// <summary> Track the existence of posts to a social network. </summary>
/// <param name="networkName"> Social network ID </param>
/// <param name="reason"> The reason of posting. (max. 32 symbols)</param>
DevToDev.Analytics.SocialNetworkPost(DevToDev.SocialNetwork networkName, string reason);
```

As a «reason» parameter we recommend that you indicate actions which encourage users to make publication.

For example:

* Start playing
* New level reached
* New building
* New ability
* Quest completed
* New item
* Collection completed
* Invitation
* Asking for help
* New Record
* Acheivement
* URL sharing
* Recommendation
* Review

and so on...

Use the current constants to specify social network:

```csharp
DevToDev.SocialNetwork.Facebook
DevToDev.SocialNetwork.Twitter
DevToDev.SocialNetwork.GooglePlus
DevToDev.SocialNetwork.Vk
// and so on...
```

Otherwise, create your own social network object.

```csharp
DevToDev.SocialNetwork socialNetwork = DevToDev.SocialNetwork.Custom(string networkName); //(max. 24 symbols)
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* Tracks the existence of posts to a social network.
* @param socialNetwork - social network Id
* @param reason - the reason of posting (max. 32 symbols)
*/
[DevToDev socialNetworkPost: (SocialNetwork *) socialNetwork withReason: (NSString *) reason];
```

As a «reason» parameter we recommend that you indicate actions which encourage users to make publication.

For example:

* Start playing
* New level reached
* New building
* New ability
* Quest completed
* New item
* Collection completed
* Invitation
* Asking for help
* New Record
* Achievement
* URL sharing
* Recommendation
* Review

and so on...

Use the current constants to specify social network:

```objectivec
Facebook
Twitter
GooglePlus
VK
//and so on...
```

Otherwise, create social network the object of your own.

```objectivec
SocialNetwork  socialNetwork = [SocialNetwork Custom: (NSString *) networkName]; //max. 24 symbols
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* Tracks the existence of posts to a social network.
* @param socialNetwork - social network Id
* @param reason - the reason of posting (max. 32 symbols)
*/
DevToDev.socialNetworkPost(socialNetwork:SocialNetwork, reason:String);
```

As a «reason» parameter we recommend that you indicate actions which encourage users to make publication.

For example:

* Start playing
* New level reached
* New building
* New ability
* Quest completed
* New item
* Collection completed
* Invitation
* Asking for help
* New Record
* Acheivement
* URL sharing
* Recommendation
* Review
* and so on...

Use the current constants to specify social network:

```javascript
SocialNetwork.VK;
SocialNetwork.TWITTER;
SocialNetwork.FACEBOOK;
SocialNetwork.GOOGLE_PLUS;
SocialNetwork.WHATS_APP;
SocialNetwork.VIBER;
SocialNetwork.EVERNOTE;
SocialNetwork.GOOGLE_MAIL;
SocialNetwork.LINKED_IN;
SocialNetwork.PINTEREST;
SocialNetwork.QZONE;
SocialNetwork.REDDIT;
SocialNetwork.RENREN;
SocialNetwork.TUMBLR;
```

Otherwise, create your own social network object:

```javascript
/**
* Custom social network object
* @param networkName - social network name (max. 24 symbols)
*/
var socialNetwork:SocialNetwork = SocialNetwork.Custom(networkName:String);
```

{% endtab %}

{% tab title="UE4" %}
Blueprint

![](https://blobscdn.gitbook.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LnBolWcwPO33iga2rY2%2F-LnTASe7tY_8XGfgeRsv%2F-LnTAUvsIEeg607aEc5u%2F143-secondarymethods-2.png?generation=1567095666609107\&alt=media)

| **Field**   | **Type** | **Description**                         |
| ----------- | -------- | --------------------------------------- |
| Social Name | FString  | Social network Id                       |
| Reason      | FString  | The reason of posting (max. 32 symbols) |

**Code**

```cpp
// Tracks the existence of posts to a social network.
// FString socialNetwork - social network Id
// FString reason - the reason of posting (max. 32 symbols)

UDevToDevBlueprintFunctionLibrary::SocialNetworkPost(const FString& socialNetwork, const FString& reason);
```

As a «reason» parameter we recommend that you indicate actions which encourage users to make publication.

| <p>For example:</p><ul><li>Start playing</li><li>New level reached</li><li>New building</li><li>New ability</li><li>Quest completed</li><li>New item</li><li>Collection completed</li><li>Invitation</li></ul> | <ul><li>Asking for help</li><li>New Record</li><li>Achievement</li><li>URL sharing</li><li>Recommendation</li><li>Review</li></ul><p>and so on...</p> |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |

| Value         | Social Network | Value | Social Network |
| ------------- | -------------- | ----- | -------------- |
| en            | Evernote       | rt    | Reddit         |
| fb            | Facebook       | rr    | Renren         |
| gm            | Google Mail    | tb    | Tumblr         |
| gp            | Google+        | tw    | Twitter        |
| in            | LinkedIn       | vk    | VK             |
| ok            | Odnoklassniki  | vb    | Viber          |
| pi            | Pinterest      | wp    | WhatsApp       |
| qq            | Qzone          |       |                |
| {% endtab %}  |                |       |                |
| {% endtabs %} |                |       |                |

## OpenUdid

Property allows to get UDID:

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* @return OpenUdid
*/
[DevToDev getOpenUdid];
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* @return Open Udid
*/
DevToDev.getOpenUdid();
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
DevToDev.SDK.OpenUdid
```

{% endtab %}

{% tab title="Web" %}

{% endtab %}

{% tab title="Unity" %}

```csharp
DevToDev.Analytics.OpenUdid
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* @return Open Udid
*/
[DevToDev getOpenUdid];
```

{% endtab %}

{% tab title="Adobe Air" %}

{% endtab %}

{% tab title="UE4" %}

{% endtab %}
{% endtabs %}

## ODIN1

Property allows to get ODIN:

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* @return ODIN1
*/
[DevToDev getOdin1];
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* @return ODIN1
*/
DevToDev.getOdin1();
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
DevToDev.SDK.ODIN
```

{% endtab %}

{% tab title="Web" %}

{% endtab %}

{% tab title="Unity" %}

```
DevToDev.Analytics.Odin1
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* @return ODIN1
*/
[DevToDev getOdin1];
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* @return ODIN1
*/
DevToDev.getOdin1();
```

{% endtab %}

{% tab title="UE4" %}

{% endtab %}
{% endtabs %}

## UUID

Property allows to get UUID:

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* @return UUID
*/
[DevToDev getUUID];
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* @return UUID
*/
DevToDev.getUUID();
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

{% endtab %}

{% tab title="Web" %}

{% endtab %}

{% tab title="Unity" %}

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* @return UUID
*/
[DevToDev getUUID];
```

{% endtab %}

{% tab title="Adobe Air" %}

{% endtab %}

{% tab title="UE4" %}

{% endtab %}
{% endtabs %}

## Debug mode

To enable the debug mode and make SDK notifications displayed in the console use this method:

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* @param BOOL isActive
*/
[DevToDev setActiveLog: (BOOL) isActive];
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* @param logLevel
*/
DevToDev.setLogLevel(LogLevel logLevel);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
//to enable logging
DevToDev.SDK.LogEnabled = true;

//to disable loging
DevToDev.SDK.LogEnabled = false;
```

{% endtab %}

{% tab title="Web" %}

```javascript
/**
* Activates console log
* @param {boolean} status
*/

devtodev.setDebugLog(status);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <summary> Enable/Disable log</summary>
/// <param name="isEnabled">Enabled/Disabled log</param>
DevToDev.Analytics.SetActiveLog(bool isEnabled);
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* @param BOOL isActive
*/
[DevToDev setActiveLog: (BOOL) isActive];
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* @param logLevel (set logLevel=1 to enable log, 0 to disable)
*/
DevToDev.setLogLevel(logLevel:int);
```

{% endtab %}

{% tab title="UE4" %}

{% endtab %}
{% endtabs %}

## Forced sending

To send events pack before it is filled or before its formation period, you can use immediate dispatch:

{% tabs %}
{% tab title="iOS" %}

```objectivec
[DevToDev sendBufferedEvents];
```

{% endtab %}

{% tab title="Android" %}

```java
DevToDev.sendBufferedEvents();
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
DevToDev.SDK.sendBufferedEvents();
```

{% endtab %}

{% tab title="Web" %}

```javascript
/**
* Sends event packet immediately
*/

devtodev.sendBufferedEvents();
```

{% endtab %}

{% tab title="Unity" %}

```csharp
DevToDev.Analytics.SendBufferedEvents();
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
[DevToDev sendBufferedEvents];
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
DevToDev.sendBufferedEvents();
```

{% endtab %}

{% tab title="UE4" %}
![](https://blobscdn.gitbook.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LnBolWcwPO33iga2rY2%2F-LnTASe7tY_8XGfgeRsv%2F-LnTAUvv56Hds_Ds1Mf9%2F143-secondarymethods-3.png?generation=1567095659242860\&alt=media)

Code

```cpp
// Sends events pack before it is filled or before its formation period

FAnalytics::Get().GetDefaultConfiguredProvider()->FlushEvents();
```

To identify a specific product promotion or strategic campaign. (for example 'Snow Boots')
{% endtab %}
{% endtabs %}

## Current SDK version

To get the version of integrated SDK, use the following method:

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* @return SDKVersion
*/
[DevToDev sdkVersion];
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* @return SDKVersion
*/
DevToDev.getSdkVersion();
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
DevToDev.SDK.GetSdkVersion();
```

{% endtab %}

{% tab title="Web" %}

```javascript
/**
* Returns SDK version
*/

devtodev.getSdkVersion();
```

{% endtab %}

{% tab title="Unity" %}

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* @return SDKVersion
*/
[DevToDev sdkVersion];
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* @return SDKVersion
*/
DevToDev.getSdkVersion();
```

{% endtab %}

{% tab title="UE4" %}

{% endtab %}
{% endtabs %}

## Set app version

{% tabs %}
{% tab title="iOS" %}

{% endtab %}

{% tab title="Android" %}

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

{% endtab %}

{% tab title="Web" %}

{% endtab %}

{% tab title="Unity" %}
To set set current application version in WEB and Windows Standalone apps use this property:

```csharp
/// <summary>  Property allows to set current application version.
/// Attention! This property is necessary for WEB and Windows Standalone apps only.
/// It will be ignored on other platforms.
/// </summary>
/// <param name="version"> Current version of your application </param>
DevToDev.Analytics.ApplicationVersion = version;
```

{% endtab %}

{% tab title="Mac OS" %}

{% endtab %}

{% tab title="Adobe Air" %}

{% endtab %}

{% tab title="UE4" %}

{% endtab %}
{% endtabs %}

## Tracking state (GDPR)

The method of limiting the processing of user data. The right to be forgotten.

This method is implemented in accordance with the GDPR requirements.

In case a user doesn’t want their data to be sent and processed in the devtodev system, a developer must send a ’false’ value to this method.

{% tabs %}
{% tab title="iOS" %}
When calling the method setTrackingAvailability with a ‘false’ value, SDK sends a command to the server to delete all user’s personal data that has been collected by devtodev from this app and a command to block the collection of any data of this user in future, and then stops sending any messages to the devtodev system.

The user will remain listed as an impersonal unit in previously aggregated metrics.

When sending a ‘true’ value, the permission to block data collection is removed.

```objectivec
/**
* The method of limiting the processing of user data. The right to be forgotten.
* @param BOOL trackingAvailable - use 'false' to erase user's personal data and stop collecting data of this user.
* 'true' if you want to resume data collection.
*/
[DevToDev setTrackingAvailability: (BOOL) trackingAvailable];
```

{% endtab %}

{% tab title="Android" %}
When calling the method setTrackingAvailability with a ‘false’ value, SDK sends a command to the server to delete all user’s personal data that has been collected by devtodev from this app and a command to block the collection of any data of this user in future, and then stops sending any messages to the devtodev system.

The user will remain listed as an impersonal unit in previously aggregated metrics.

When sending a ‘true’ value, the permission to block data collection is removed.

```java
/**
* The method of limiting the processing of user data. The right to be forgotten.
* @param isAvailable - send 'false' to erase user's personal data and stop collecting data of this user.
* Send 'true' if you want to resume data collection.
*/
DevToDev.setTrackingAvailability(boolean isAvailable);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}
In the case of using TrackingAvailability property with a ‘false’ value, SDK sends a command to the server to delete all user’s personal data that has been collected by devtodev from this app and a command to block the collection of any data of this user in future, and then stops sending any messages to the devtodev system.

The user will remain listed as an impersonal unit in previously aggregated metrics.

In the case of using TrackingAvailability property with a ‘true’ value, the permission to block data collection is removed.

```csharp
/// <summary> The Property of limiting the processing of user data. The right to be forgotten.
/// Use 'false' to erase user's personal data and stop collecting data of this user or 'true'
/// if you want to resume data collection.</summary>

DevToDev.SDK.TrackingAvailability = false/true;
```

{% endtab %}

{% tab title="Web" %}
When calling the method setTrackingAvailability with a ‘false’ value, SDK sends a command to the server to delete all user’s personal data that has been collected by devtodev from this app and a command to block the collection of any data of this user in future, and then stops sending any messages to the devtodev system.

The user will remain listed as an impersonal unit in previously aggregated metrics.

When sending a ‘true’ value, the permission to block data collection is removed.

```javascript
/**
* The method of limiting the processing of user data. The right to be forgotten.
* @param {boolean} status - send 'false' to erase user's personal data and stop collecting data of this user.
* Send 'true' if you want to resume data collection.
*/
devtodev.setTrackingAvailability(status);
```

{% endtab %}

{% tab title="Unity" %}
In the case of using TrackingAvailability property with a ‘false’ value, SDK sends a command to the server to delete all user’s personal data that has been collected by devtodev from this app and a command to block the collection of any data of this user in future, and then stops sending any messages to the devtodev system.

The user will remain listed as an impersonal unit in previously aggregated metrics.

In the case of using TrackingAvailability property with a ‘true’ value, the permission to block data collection is removed.

```csharp
/**
* The property of limiting the processing of user data. The right to be forgotten.
* Use 'false' to erase user's personal data and stop collecting data of this user or 'true'
* if you want to resume data collection.</summary>
*/
DevToDev.Analytics.TrackingAvailability = false/true;
```

{% endtab %}

{% tab title="Mac OS" %}
When calling the method setTrackingAvailability with a ‘false’ value, SDK sends a command to the server to delete all user’s personal data that has been collected by devtodev from this app and a command to block the collection of any data of this user in future, and then stops sending any messages to the devtodev system.

The user will remain listed as an impersonal unit in previously aggregated metrics.

When sending a ‘true’ value, the permission to block data collection is removed.

```objectivec
/**
* The method of limiting the processing of user data. The right to be forgotten.
* @param BOOL trackingAvailable - use 'false' to erase user's personal data and stop collecting data of this user.
* 'true' if you want to resume data collection.
*/
[DevToDev setTrackingAvailability: (BOOL) trackingAvailable];
```

{% endtab %}

{% tab title="Adobe Air" %}
When calling the method setTrackingAvailability with a ‘false’ value, SDK sends a command to the server to delete all user’s personal data that has been collected by devtodev from this app and a command to block the collection of any data of this user in future, and then stops sending any messages to the devtodev system.

The user will remain listed as an impersonal unit in previously aggregated metrics.

When sending a ‘true’ value, the permission to block data collection is removed.

```javascript
/**
* The method of limiting the processing of user data. The right to be forgotten.
* @param isTrackingAvailable - send 'false' to erase user's personal data and stop collecting data of this user.
* Send 'true' if you want to resume data collection.
*/
DevToDev.setTrackingAvailability(isTrackingAvailable:Boolean)
```

{% endtab %}

{% tab title="UE4" %}

{% endtab %}
{% endtabs %}


# User profile

{% hint style="danger" %}
**This generation of SDK is deprecated and is no longer supported.**\
Information about the [current version can be found here](/integration/integration-of-sdk-v2/setting-up-events/user-profile).
{% endhint %}

In addition to basic methods, you can observe and change the user profiles data. A user profile is the set of properties describing the user, which can be divided into 4 groups:

{% tabs %}
{% tab title="iOS" %}

1. Cross-platform or custom user identifier. If this identifier is not set by a developer, then the device identifier is used.
2. Automatically collected properties, including data about user's device, geography, app version, SDK, and some other data which can be received from SDK.
3. The default set of user properties, which can be set by a developer. The set of this parameters works with separate methods. This set includes the data of the user's name, sex, age, e-mail, phone number and URL of user picture. Also, this set includes the mark of a user as a cheater.
4. Custom set of user properties. In this case, a developer sets any user data he/she needs to know. The data is set in key-value format and can be numeric, string, array, or boolean. Each project can have up to 30 custom user properties.
   {% endtab %}

{% tab title="Android" %}

1. Cross-platform or custom user identifier. If this identifier is not set by a developer, then the device identifier is used.
2. Automatically collected properties, including data about user's device, geography, app version, SDK, and some other data which can be received from SDK.
3. The default set of user properties, which can be set by a developer. The set of this parameters works with separate methods. This set includes the data of the user's name, sex, age, e-mail, phone number, and URL of user picture. Also, this set includes the mark of a user as a cheater.
4. Custom set of user properties. In this case, a developer sets any user data he/she needs to know. The data is set in key-value format and can be numeric, string, array, or boolean. Each project can have up to 30 custom user properties.
   {% endtab %}

{% tab title="Windows 8.1 and 10" %}

1. Cross-platform or custom user identifier. If this identifier is not set by developer, then the device identifier is used.
2. Automatically collected properties, including data about user's device, geography, app version, SDK, and some other data which can be received from SDK.
3. Default set of user properties, which can be set by developer. The set of this parameters works with separate methods. This set includes the data of user's name, sex, age, e-mail, phone-number and url of user picture. Also this set includes the mark of user as cheater.
4. Custom set of user properties. In this case developer sets any user data he/she needs to know. The data is set in key-value format and can be numeric, string, array or boolean. Each project can have up to 30 custom user properties.
   {% endtab %}

{% tab title="Web" %}

1. Cross-platform or custom user identifier. If this identifier is not set by developer, then the identifier which was set during the initialization is used.
2. Automatically collected properties, including data about user's device, geography, app version, SDK, and some other data which can be received from SDK.
3. Basic set of user properties, which can be set by developer. The set of this parameters works with separate methods. This set includes the data of user's name, sex, age, e-mail, phone-number and url of user picture. Also this set includes the mark of user as cheater.
4. Custom set of user properties. In this case developer sets any user data he/she needs to know. The data is set in key-value format and can be numeric, string, array or boolean. Each project can have up to 30 custom user properties.
   {% endtab %}

{% tab title="Unity" %}

1. Cross-platform or custom user identifier. If this identifier is not set by developer, then the device identifier is used.
2. Automatically collected properties, including data about user's device, geography, app version, SDK, and some other data which can be received from SDK.
3. Default set of user properties, which can be set by developer. The set of this parameters works with separate methods. This set includes the data of user's name, sex, age, e-mail, phone-number and url of user picture. Also this set includes the mark of user as cheater.
4. Custom set of user properties. In this case developer sets any user data he/she needs to know. The data is set in key-value format and can be numeric, string, array or boolean. Each project can have up to 30 custom user properties.
   {% endtab %}

{% tab title="Mac OS" %}

1. Cross-platform or custom user identifier. If this identifier is not set by developer, then the device identifier is used.
2. Automatically collected properties, including data about user's device, geography, app version, SDK, and some other data which can be received from SDK.
3. Default set of user properties, which can be set by developer. The set of this parameters works with separate methods. This set includes the data of user's name, sex, age, e-mail, phone-number and url of user picture. Also this set includes the mark of user as cheater.
4. Custom set of user properties. In this case developer sets any user data he/she needs to know. The data is set in key-value format and can be numeric, string, array or boolean. Each project can have up to 30 custom user properties.
   {% endtab %}

{% tab title="Adobe Air" %}

1. Cross-platform or custom user identifier. If this identifier is not set by developer, then the device identifier is used.
2. Automatically collected properties, including data about user's device, geography, app version, SDK, and some other data which can be received from SDK.
3. Default set of user properties, which can be set by developer. The set of this parameters works with separate methods. This set includes the data of user's name, sex, age, e-mail, phone-number and url of user picture. Also this set includes the mark of user as cheater.
4. Custom set of user properties. In this case developer sets any user data he/she needs to know. The data is set in key-value format and can be numeric, string, array or boolean. Each project can have up to 30 custom user properties.
   {% endtab %}

{% tab title="UE4" %}

1. Cross-platform or custom user identifier. If this identifier is not set by developer, then the device identifier is used.
2. Automatically collected properties, including data about user's device, geography, app version, SDK, and some other data which can be received from SDK.
3. Default set of user properties, which can be set by developer. The set of this parameters works with separate methods. This set includes the data of user's name, sex, age, e-mail, phone-number and url of user picture. Also this set includes the mark of user as cheater.
4. Custom set of user properties. In this case developer sets any user data he/she needs to know. The data is set in key-value format and can be numeric, string, array or boolean. Each project can have up to 30 custom user properties.
   {% endtab %}
   {% endtabs %}

You can segment users by all the properties in My Apps section of an application.

## Cross-platform user ID

{% tabs %}
{% tab title="iOS" %}
This method is used for user initialization in the applications that are the parts of cross-platform project.

We recommend you to apply this method before the SDK initialization, otherwise the user identifier from the previous session will be used since the SDK initialization moment till the *setUserID* method call.

If your cross-platform application is supposed to be used without cross-platform authorization, don't use the *setUserID* method or use the empty string ("") as the user identifier. SDK will assign the unique identifier to user. This identifier will be used until the real cross-platform identifier is assigned to the user.

{% hint style="warning" %}
If your application allows user to re-login (changing the user during the working session of the application), then the *setUserID* method should be called just after the authorization. You don't need to call the SDK initialization one more time.&#x20;
{% endhint %}

```objectivec
/**
* Initializes the user with the specified cross-platform identifier
* @param NSString userId - unique cross-platform user ID used
* for user identification on your server.
*/
[DevToDev setUserId: (NSString *) userId];
```

To see which identifier is used at the moment:

```objectivec
/**
* Returns current cross-platform user id
* @return userId - current cross-platform user id
*/
[DevToDev getUserId];
```

{% endtab %}

{% tab title="Android" %}
This method is used for user initialization in the applications which are the parts of cross-platform project.

We recommend you to apply this method before the SDK initialization, otherwise the user identifier from the previous session will be used since the SDK initialization moment till the *setUserID* method call.

If your cross-platform application supposes to be used without cross-platform authorization, don't use the *setUserID* method or use the empty string ("") as the user identifier. SDK will assign the unique identifier to user. This identifier will we used until the real cross-platform identifier assigns to the user.

{% hint style="warning" %}
If your application allows user to re-login (changing the user during the working session of application), then the *setUserID* method should be called just after the authorization. You don't need to call the SDK initialization one more time.&#x20;
{% endhint %}

```java
/**
* Initializes the user with the specified cross-platform identifier
* @param String userId - unique cross-platform user ID used
* for user identification on your server.
*/
DevToDev.setUserId(String userId);
```

To see which identifier is used at the moment:

```java
/**
* Returns current cross-platform user id
* @return userId - current cross-platform user id (or null, if sdk not initialized)
*/
DevToDev.getUserId();
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}
This method is used for user initialization in the applications which are the parts of cross-platform project.

We recommend you to apply this method before the SDK initialization, otherwise the user identiticator from the previous session will be used since the SDK initialization moment till the *UserID* property call.

If your cross-platform application supposes to be used without cross-platform authorization, don't use the *UserID* property or use the empty string ("") as the user identifier. SDK will assign the unique identifier to user. This identifier will we used until the real cross-platform identifier assigns to the user.

{% hint style="warning" %}
If your application allows user to re-login (changing the user during the working session of application), then the *UserID* property should be called just after the authorization. You don't need to call the SDK initialization one more time.&#x20;
{% endhint %}

```csharp
/**
* Initializes the user with the specified cross-platform identifier
* property allows to get and to set unique cross-platform user ID used
* for user identification on your server.
*/
DevToDev.SDK.UserId = userId;
```

This property also allows you to see which identifier is used at the moment.
{% endtab %}

{% tab title="Web" %}
This method is used for user initialization in the applications which are the parts of cross-platform project. You also can use this identifier in non-crossplatform projects, but in your app the own unique user identifier is used.

We recommend you to apply this method before the SDK initialization, otherwise the user identifier from the previous session will be used since the SDK initialization moment till the setCrossplatformUserId method call.

If your cross-platform application supposes to be used without cross-platform authorization, don't use the setCrossplatformUserId method or use the empty string ("") as the user identifier. SDK will assign the unique identifier to user. This identifier will be used until the real cross-platform identifier assigns to the user.

```javascript
/**
* Initializes the user with the specified cross-platform identifier
* @param {string} crossplatformUserId - unique cross-platform user ID used
* for user identification on your server.
*/

devtodev.setCrossplatformUserId(crossplatformUserId);
```

{% hint style="warning" %}
If your application allows user to re-login (changing the user during the working session of application), then the setCrossplatformUserId method should be called just after the authorization. You don't need to call the SDK initialization one more time.&#x20;
{% endhint %}

To see which identifier is used at the moment:

```javascript
/**
* Returns current cross-platform user id
* @return crossPlatformUserId - current cross-platform user id
*/

devtodev.getCrossplatformUserId();
```

{% endtab %}

{% tab title="Unity" %}
This method is used for user initialization in the applications which are the parts of cross-platform project.

We recommend you to apply this method before the SDK initialization, otherwise the user identifier from the previous session will be used since the SDK initialization moment till the *UserID* property call.

If your cross-platform application supposes to be used without cross-platform authorization, don't use the *UserID* property or use the empty string ("") as the user identifier. SDK will assign the unique identifier to user. This identifier will we used until the real cross-platform identifier assigns to the user.

{% hint style="warning" %}
If your application allows user to re-login (changing the user during the working session of application), then the *UserID* property should be called just after the authorization. You don't need to call the SDK initialization one more time.
{% endhint %}

```csharp
/// <summary>
/// Initializes the user with the specified cross-platform identifier
/// property allows to get and to set unique cross-platform user ID used
/// for user identification on your server.</summary>
DevToDev.Analytics.UserId = userId;
```

This property also allows you to see which identifier is used at the moment.
{% endtab %}

{% tab title="Mac OS" %}
This method is used for user initialization in the applications which are the parts of cross-platform project.

We recommend you to apply this method before the SDK initialization, otherwise the user identifier from the previous session will be used since the SDK initialization moment till the *setUserID* method call.

If your cross-platform application supposes to be used without cross-platform authorization, don't use the *setUserID* method or use the empty string ("") as the user identifier. SDK will assign the unique identifier to user. This identifier will we used until the real cross-platform identifier assigns to the user.

{% hint style="warning" %}
If your application allows user to re-login (changing the user during the working session of application), then the *setUserID* method should be called just after the authorization. You don't need to call the SDK initialization one more time.&#x20;
{% endhint %}

```objectivec
/**
* Initializes the user with the specified cross-platform identifier
* @param NSString userId - unique cross-platform user ID used
* for user identification on your server.
*/
[DevToDev setUserId: (NSString *) userId];
```

To see which identifier is used at the moment:

```objectivec
/**
* Returns current cross-platform user id
* @return userId - current cross-platform user id
*/
[DevToDev getUserId];
```

{% endtab %}

{% tab title="Adobe Air" %}
This method is used for user initialization in the applications which are the parts of cross-platform project.

We recommend you to apply this method before the SDK initialization, otherwise the user identifier from the previous session will be used since the SDK initialization moment till the *setUserID* method call.

If your cross-platform application supposes to be used without cross-platform authorization, don't use the *setUserID* method or use the empty string ("") as the user identifier. SDK will assign the unique identifier to user. This identifier will we used until the real cross-platform identifier assigns to the user.

{% hint style="warning" %}
If your application allows user to re-login (changing the user during the working session of application), then the *setUserID* method should be called just after the authorization. You don't need to call the SDK initialization one more time.
{% endhint %}

```javascript
/**
* Initializes the user with the specified cross-platform identifier
* @param userId - unique cross-platform user ID used for user identification on your server.
*/
DevToDev.setUserId(userId:String);
```

To see which identifier is used at the moment:

```javascript
/**
* Returns current cross-platform user id
* @return userId - current cross-platform user id (string or null, if sdk not initialized)
*/
DevToDev.getUserId();
```

{% endtab %}

{% tab title="UE4" %}
This method is used for user initialization in the applications which are the parts of cross-platform project.

We recommend you to apply this method before the SDK initialization, otherwise the user identifier from the previous session will be used since the SDK initialization moment till the setUserID method call.

If your cross-platform application supposes to be used without cross-platform authorization, don't use the setUserID method or use the empty string ("") as the user identifier. SDK will assign the unique identifier to user. This identifier will we used until the real cross-platform identifier assigns to the user.

{% hint style="warning" %}
If your application allows user to re-login (changing the user during the working session of application), then the setUserID method should be called just after the authorization. You don't need to call the SDK initialization one more time.
{% endhint %}

**Blueprint**

![](/files/-LnlSivDHI3bsBDqop9L)

| Field   | Type    | Description                   |
| ------- | ------- | ----------------------------- |
| User Id | FString | Unique cross-platform user id |

**Code**

```cpp
// Initializes the user with the specified cross-platform identifier
// FString activeUserId - unique cross-platform user id

FAnalytics::Get().GetDefaultConfiguredProvider()->SetUserID(FString activeUserId);
```

To see which identifier is used at the moment:

**Blueprint**

![](/files/-LnlSivFOX0jnkRC4uTo)

**Code**

```cpp
// Returns current cross-platform user id

FAnalytics::Get().GetDefaultConfiguredProvider()->GetUserID();
```

{% endtab %}
{% endtabs %}

## Replace Cross-platform user ID

{% tabs %}
{% tab title="iOS" %}
If it is possible to replace the user identifier in your application (for example, to make changes in the login/user id for a particular user), use this method at the moment of replacing the identifier.

{% hint style="danger" %}
Don't use this method if you're going to perform the user's re-login.&#x20;
{% endhint %}

```objectivec
/**
* Replaces current cross-platform user id
* Attention! Don't use this method if you're going to perform the user's relogin.
* @param NSString prevUserId - previous cross-platform user ID
* @param NSString userId - new cross-platform user ID
*/
[DevToDev replaceUserId: (NSString *) prevUserId to: (NSString *) userId];
```

{% endtab %}

{% tab title="Android" %}
If it is possible to replace the user identifier in your application (say, to make changes in the login/user id for particular user), use this method at the moment of replacing the identifier.

{% hint style="danger" %}
Don't use this method if you're going to perform the user's re-login.&#x20;
{% endhint %}

```java
/**
* Replaces current cross-platform user id
* Attention! Don't use this method if you're going to perform the user's relogin.
* @param String prevUserId - previous cross-platform user ID
* @param String userId - new cross-platform user ID
*/
DevToDev.replaceUserId(String prevUserId, String userId);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}
If it is possible to replace the user identifier in your application (say, to make changes in the login/user id for particular user), use this method at the moment of replacing the identifier.

{% hint style="danger" %}
Don't use this method if you're going to perform the user's re-login.&#x20;
{% endhint %}

```csharp
/**
* Replaces current cross-platform user id
* Attention! Don't use this method if you're going to perform the user's relogin.
* <param name="prevUserId">Old user identifier</param>
* <param name="userId">New user identifier</param>
*/
DevToDev.SDK.ReplaceUserId(string prevUserId, string userId);
```

{% endtab %}

{% tab title="Web" %}
If it is possible to replace the cross-platform user identifier in your application (say, to make changes in the login/user id for particular user), use this method at the moment of replacing the identifier.

{% hint style="danger" %}
Don't use this method if you're going to perform the user's re-login.&#x20;
{% endhint %}

```javascript
/**
* Replaces current cross-platform user id
* Attention! Don't use this method if you're going to perform the user's relogin.
* @param {string} newCrossPlatformID - new cross-platform user ID
*/

devtodev.replaceCrossplatformUserId(newCrossPlatformID);
```

{% endtab %}

{% tab title="Unity" %}
If it is possible to replace the user identifier in your application (say, to make changes in the login/user id for particular user), use this method at the moment of replacing the identifier.

{% hint style="danger" %}
Don't use this method if you're going to perform the user's re-login.&#x20;
{% endhint %}

```csharp
/// <summary> Replaces current cross-platform user id. 
/// Attention! Don't use this method if you're going to perform the user's relogin.</summary>
/// <param name="prevUserId"> Old user identifier </param>
/// <param name="userId"> New user identifier </param>
DevToDev.Analytics.ReplaceUserId(string prevUserId, string userId);
```

{% endtab %}

{% tab title="Mac OS" %}
If it is possible to replace the user identifier in your application (say, to make changes in the login/user id for particular user), use this method at the moment of replacing the identifier.

{% hint style="danger" %}
Don't use this method if you're going to perform the user's re-login.&#x20;
{% endhint %}

```objectivec
/**
* Replaces current cross-platform user id
* Attention! Don't use this method if you're going to perform the user's relogin.
* @param NSString prevUserId - previous cross-platform user ID
* @param NSString userId - new cross-platform user ID
*/
[DevToDev replaceUserId: (NSString *) prevUserId to: (NSString *) userId];
```

{% endtab %}

{% tab title="Adobe Air" %}
If it is possible to replace the user identifier in your application (say, to make changes in the login/user id for particular user), use this method at the moment of replacing the identifier.&#x20;

{% hint style="danger" %}
Don't use this method if you're going to perform the user's re-login.&#x20;
{% endhint %}

```javascript
/**
* Replaces current cross-platform user id
* Attention! Don't use this method if you're going to perform the user's relogin.
* @param prevUserId - previous cross-platform user ID
* @param userId - new cross-platform user ID
*/
DevToDev.replaceUserId(prevUserId:String, userId:String);
```

{% endtab %}

{% tab title="UE4" %}
If it is possible to replace the user identifier in your application (say, to make changes in the login/user id for particular user), use this method at the moment of replacing the identifier.

{% hint style="danger" %}
Don't use this method if you're going to perform the user's re-login.&#x20;
{% endhint %}

***Blueprint***

![](/files/-LnlSivHuib6ScMRM2JZ)

| Field | Type    | Description     |
| ----- | ------- | --------------- |
| From  | FString | Current user Id |
| To    | FString | New user Id     |

**Code**

```cpp
// Replaces cross-platform user id
// FString from - current user id
// FString to - new user id

UDevToDevBlueprintFunctionLibrary::ReplaceUserId(const FString& from, const FString& to);
```

{% endtab %}
{% endtabs %}

## Current user level

{% tabs %}
{% tab title="iOS" %}
This method is used in cross-platform applications and applications with data synchronization.

This method is required for user's level data initialization. We recommend you to use the *setCurrentLevel* method just after the user initialization (using the *setUserID* method).

{% hint style="warning" %}
Don't use the setCurrentLevel method at the moment of user's level up. We recommend you to use the levelUp method in this case.&#x20;
{% endhint %}

```objectivec
/**
* Initializes the current user level. Required if level feature used in the app.
* @param NSUInteger level - current game level of the player.
*/
[DevToDev setCurrentLevel: (NSUInteger) level];
```

{% endtab %}

{% tab title="Android" %}
This method is used in cross-platform applications and applications with data synchronization.

This method is required for user's level data initialization. We recommend you to use the *setCurrentLevel* method just after the user initialization (using the *setUserID* method).

{% hint style="warning" %}
Don't use the setCurrentLevel method at the moment of user's level up. We recommend you to use the levelUp method in this case.&#x20;
{% endhint %}

```java
/**
* Initializes the current user level. Required if level feature used in the app.
* @param int level - current game level of the player.
*/
DevToDev.setCurrentLevel(int level);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}
This method is used in cross-platform applications and applications with data synchronization.

This method is required for user's level data initialization. We recommend you to use the *SetCurrentLevel* method just after the user initialization (using the *UserID* method).

{% hint style="warning" %}
Don't use the SetCurrentLevel method at the moment of user's level up. We recommend you to use the levelUp method in this case.&#x20;
{% endhint %}

```csharp
/**
* Initializes the current user level. Required if level feature used in the app.
* <param name="level">Current game level of the player</param>
*/
DevToDev.SDK.SetCurrentLevel(int level);
```

{% endtab %}

{% tab title="Web" %}
If your app uses the user's level mark, we recommend to use this method after every SDK initialization, as soon as data of user's level is available to the application.

```javascript
/**
* Initializes the current user level. Required if level feature used in the app.
* @param {number} currentUserLevel - current game level of the player.
*/

devtodev.setCurrentLevel(currentUserLevel);
```

{% endtab %}

{% tab title="Unity" %}
This method is used in cross-platform applications and applications with data synchronization.

This method is required for user's level data initialization. We recommend you to use the *SetCurrentLevel* method just after the user initialization (using the *UserID* method).

{% hint style="warning" %}
Don't use the SetCurrentLevel method at the moment of user's level up. We recommend you to use the levelUp method in this case.&#x20;
{% endhint %}

```csharp
/// <summary> Initializes the current user level. Required if level feature used in the app. </summary>
/// <param name="level"> current game level of the player </param>
DevToDev.Analytics.SetCurrentLevel(int level);
```

{% endtab %}

{% tab title="Mac OS" %}
This method is used in cross-platform applications and applications with data synchronization.

This method is required for user's level data initialization. We recommend you to use the *setCurrentLevel* method just after the user initialization (using the *setUserID* method).

{% hint style="warning" %}
Don't use the setCurrentLevel method at the moment of user's level up. We recommend you to use the levelUp method in this case.&#x20;
{% endhint %}

```objectivec
/**
* Initializes the current user level. Required if level feature used in the app.
* @param NSUInteger level - current game level of the player.
*/
[DevToDev setCurrentLevel: (NSUInteger) level];
```

{% endtab %}

{% tab title="Adobe Air" %}
This method is used in cross-platform applications and applications with data synchronization.

This method is required for user's level data initialization. We recommend you to use the *setCurrentLevel* method just after the user initialization (using the *setUserID* method).

{% hint style="warning" %}
Don't use the setCurrentLevel method at the moment of user's level up. We recommend you to use the levelUp method in this case.&#x20;
{% endhint %}

```javascript
/**
* Initializes the current user level. Required if level feature used in the app.
* @param level - current game level of the player.
*/
DevToDev.setCurrentLevel(level:int);
```

{% endtab %}

{% tab title="UE4" %}
This method is used in cross-platform applications and applications with data synchronization.

This method is required for user's level data initialization. We recommend you to use the *setCurrentLevel* method just after the user initialization (using the *setUserID* method).

{% hint style="warning" %}
Don't use the setCurrentLevel method at the moment of user's level up. We recommend you to use the levelUp method in this case.&#x20;
{% endhint %}

**Blueprint**

![](/files/-LnlSivJpUzBrMoXn2Ep)

| Field | Type  | Description        |
| ----- | ----- | ------------------ |
| Level | int32 | Current user level |

**Code**

```cpp
// Initializes the current user level
// int32 level - current user level

UDevToDevBlueprintFunctionLibrary::SetCurrentLevel(int32 level)
```

{% endtab %}
{% endtabs %}

## Cheater

In case you have your own methods of determining cheaters in the application, you can have such users marked. Payments made by them will not be taken into account in the statistics.

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* Mark user if it's cheater.
* @param BOOL isCheater - true if user is a cheater
*/
DevToDev.activeUser.cheater = isCheater;
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* Mark user if it's cheater.
* @param boolean isCheater - true if user is a cheater
*/
DevToDev.getActivePlayer().setCheater(boolean isCheater);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
* Mark user if it's cheater.
* <param name="isCheater">true if user is a cheater</param>
*/
DevToDev.SDK.ActiveUser.SetCheater(boolean isCheater);
```

{% endtab %}

{% tab title="Web" %}

```javascript
/**
* Mark user if it's cheater.
* @param {boolean} isCheater - true if user is a cheater
*/

devtodev.user.cheater(isCheater);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <summary> Mark user if it's cheater. </summary>
/// <param name="isCheater"> true if user is a cheater </param>
DevToDev.Analytics.ActiveUser.Cheater = isCheater;
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* Mark user if it's cheater.
* @param BOOL isCheater - true if user is a cheater
*/
DevToDev.activeUser.cheater = isCheater;
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* Mark user if it's cheater.
* @param isCheater - true if user is a cheater
*/
DevToDev.getActiveUser().SetCheater(isCheater:Boolean);
```

{% endtab %}

{% tab title="UE4" %}
**Blueprint**

![](/files/-LnlSivL2BCr9YbxaOzK)

| Field     | Type | Description               |
| --------- | ---- | ------------------------- |
| isCheater | bool | True if user is a cheater |

**Code**

```cpp
UPeopleLibrary::Cheater(bool cheater);
```

{% endtab %}
{% endtabs %}

## Name

User's name. Default user profile property.

{% hint style="warning" %}
We strongly recommend not to use this property because it refers to [personal data](https://gdpr-info.eu/issues/personal-data/).&#x20;
{% endhint %}

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* Track user's name
* @param NSString name - User's name.
*/
DevToDev.activeUser.name = name;
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* Track user's name
* @param String name - User's name
*/
DevToDev.getActivePlayer().setName(String name);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
* Track user's name
* <param name="name">User's name</param>
*/
DevToDev.SDK.ActiveUser.SetName(string name);
```

{% endtab %}

{% tab title="Web" %}

```javascript
/**
* Track user's name
* @param {string} name - User's name.
*/

devtodev.user.name(name);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <summary> Track user's name </summary>
/// <param name="name"> User's name </param>
DevToDev.Analytics.ActiveUser.Name = name;
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* Track user's name
* @param NSString name - User's name.
*/
DevToDev.activeUser.name = name;
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* Track user's name
* @param name - User's name.
*/
DevToDev.getActiveUser().SetName(name:String);
```

{% endtab %}

{% tab title="UE4" %}
**Blueprint**

\--

| **Field** | **Type** | **Description** |
| --------- | -------- | --------------- |
| Name      | FString  | User's name     |

**Code**

```cpp
UPeopleLibrary::Name(const FString& name);
```

{% endtab %}
{% endtabs %}

## Age

User's age in years. Default user profile property.

{% hint style="warning" %}
We strongly recommend not to use this property because it refers to [personal data](https://gdpr-info.eu/issues/personal-data/).&#x20;
{% endhint %}

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* Track user's age
* @param NSNumber age - User's age
*/
DevToDev.activeUser.age = age;
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* Track user's age
* @param int age - User's age
*/
DevToDev.getActivePlayer().setAge(int age);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
* Track user's age
* <param name="age">User's age</param>
*/
DevToDev.SDK.ActiveUser.SetAge(int age);
```

{% endtab %}

{% tab title="Web" %}

```javascript
/**
* Track user's age
* @param {number} age - User's age.
*/

devtodev.user.age(age);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <summary> Track user's age </summary>
/// <param name="age"> User's age </param>
DevToDev.Analytics.ActiveUser.Age = age;
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* Track user's age
* @param NSNumber age - User's age
*/
DevToDev.activeUser.age = age;
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* Track user's age
* @param age - User's age in years
*/
DevToDev.getActiveUser().SetAge(age:int);
```

{% endtab %}

{% tab title="UE4" %}
**Blueprint**

![](/files/-LnlSivSI2XXFhcBXPRC)

| **Field** | **Type** | **Description**     |
| --------- | -------- | ------------------- |
| Age       | int32    | User's age in years |

**Code**

```cpp
UPeopleLibrary::Age(int32 age);
```

{% endtab %}
{% endtabs %}

## Gender

User's gender. Default user profile property.

{% hint style="warning" %}
We strongly recommend not to use this property because it refers to [personal data](https://gdpr-info.eu/issues/personal-data/).&#x20;
{% endhint %}

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* Track user's
* @param DTDGender gender - User's gender. (Unknown, Male, Female).
*/
DevToDev.activeUser.gender = gender;
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* Track user's gender
* @param Gender gender - User's gender. (Unknown, Male, Female).
*/
DevToDev.getActivePlayer().setGender(Gender gender);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
* Track user's gender.
* <param name="gender">User's gender. (Unknown, Male, Female)</param>
*/
DevToDev.SDK.ActiveUser.SetGender(DevToDev.Gender gender);
```

{% endtab %}

{% tab title="Web" %}

```javascript
/**
* Track user's gender
* @param {number} gender - User's gender. (0 - Unknown, 1 - Male, 2 - Female).
*/

devtodev.user.gender(gender);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <summary> Track user's gender </summary>
/// <param name="gender"> User's gender. (Unknown, Male, Female) </param>
DevToDev.Analytics.ActiveUser.Gender = gender;
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* Track user's
* @param DTDGender gender - User's gender. (Unknown, Male, Female).
*/
DevToDev.activeUser.gender = gender;
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* Track user's
* @param gender - User's gender. (Unknown == 0, Male == 1, Female == 2).
*/
DevToDev.getActiveUser().SetGender(gender:int);
```

{% endtab %}

{% tab title="UE4" %}
**Blueprint**

![](/files/-LnlSivVSZY3bx0r-X1Q)

| **Field** | **Type** | **Description**                             |
| --------- | -------- | ------------------------------------------- |
| Gender    | FString  | User's gender ('male', 'female', 'unknown') |

**Code**

```cpp
UPeopleLibrary::Gender(const FString& InGender);
```

{% endtab %}
{% endtabs %}

## E-mail

User's e-mail. Default user profile property.

{% hint style="warning" %}
We strongly recommend not to use this property because it refers to [personal data](https://gdpr-info.eu/issues/personal-data/).&#x20;
{% endhint %}

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* Track user's e-mail
* @param NSString email - User's e-mail.
*/
DevToDev.activeUser.email = email;
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* Track user's e-mail
* @param String email - User's e-mail.
*/
DevToDev.getActivePlayer().setEmail(String email);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
* Track user's e-mail
* <param name="email">User's e-mail</param>
*/
DevToDev.SDK.ActiveUser.SetEmail(string email);
```

{% endtab %}

{% tab title="Web" %}

```javascript
/**
* Track user's e-mail
* @param {string} email - User's e-mail.
*/
devtodev.user.email(email);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <summary> Track user's e-mail </summary>
/// <param name="email"> User's e-mail </param>
DevToDev.Analytics.ActiveUser.Email = email;
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* Track user's e-mail
* @param NSString email - User's e-mail.
*/
DevToDev.activeUser.email = email;
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* Track user's e-mail
* @param email - User's e-mail.
*/
DevToDev.getActiveUser().SetEmail(email:String);
```

{% endtab %}

{% tab title="UE4" %}
**Blueprint**

![](/files/-LnlSivYLHydfKIMMBxw)

| **Field** | **Type** | **Description** |
| --------- | -------- | --------------- |
| Email     | FString  | User's e-mail   |

**Code**

```cpp
UPeopleLibrary::Email(const FString& email);
```

{% endtab %}
{% endtabs %}

## Phone number

User's phone. Default user profile property.

{% hint style="warning" %}
We strongly recommend not to use this property because it refers to [personal data](https://gdpr-info.eu/issues/personal-data/).&#x20;
{% endhint %}

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* Track user's phone number
* @param NSString phoneNumber - User's phone number.
*/
DevToDev.activeUser.phone = phoneNumber;
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* Track user's phone number
* @param String phoneNumber - User's phone number.
*/
DevToDev.getActivePlayer().setPhone(String phoneNumber);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
* Track user's phone number
* <param name="phoneNumber">User's phone number</param>
*/
DevToDev.SDK.ActiveUser.SetPhone(string phoneNumber);
```

{% endtab %}

{% tab title="Web" %}

```javascript
/**
* Track user's phone number
* @param {string} phone_number - User's phone number.
*/

devtodev.user.phone(phone_number);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <summary> Track user's phone number </summary>
/// <param name="phoneNumber"> User's phone number </param>
DevToDev.Analytics.ActiveUser.Phone = phoneNumber;
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* Track user's phone number
* @param NSString phoneNumber - User's phone number.
*/
DevToDev.activeUser.phone = phoneNumber;
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* Track user's phone number
* @param phoneNumber - User's phone number.
*/
DevToDev.getActiveUser().SetPhone(phoneNumber:String);
```

{% endtab %}

{% tab title="UE4" %}
**Blueprint**

![](/files/-LnlSiv_UDzH-njJXlUj)

| **Field** | **Type** | **Description** |
| --------- | -------- | --------------- |
| Phone     | FString  | User's phone    |

**Code**

```cpp
UPeopleLibrary::Phone(const FString& phone);
```

{% endtab %}
{% endtabs %}

## Photo

User's photo URL. Default user profile property.

{% hint style="warning" %}
We strongly recommend not to use this property because it refers to [personal data](https://gdpr-info.eu/issues/personal-data/).&#x20;
{% endhint %}

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* Track user's photo URL
* @param NSString photoUrl - User's photo URL.
*/
DevToDev.activeUser.photo = photoUrl;
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* Track user's photo URL
* @param String photoUrl - User's photo url.
*/
DevToDev.getActivePlayer().setPhoto(String photoUrl);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
* Track user's photo URL
* <param name="photoUrl">User's photo url</param>
*/
DevToDev.SDK.ActiveUser.SetPhoto(string photoUrl);
```

{% endtab %}

{% tab title="Web" %}

```javascript
/**
* Track user's photo URL
* @param {string} photo_url - User's phone number.
*/

devtodev.user.photo(photo_url);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <summary> Track user's photo URL </summary>
/// <param name="photoUrl"> User's photo url </param>
DevToDev.Analytics.ActiveUser.Photo = photoUrl;
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* Track user's photo URL
* @param NSString photoUrl - User's photo URL.
*/

DevToDev.activeUser.photo = photoUrl;
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* Track user's photo URL
* @param photoUrl - User's photo url.
*/
DevToDev.getActiveUser().SetPhoto(photoUrl:String);
```

{% endtab %}

{% tab title="UE4" %}
**Blueprint**

![](/files/-LnlSivb8KoQ8eyMCS8Q)

| **Field** | **Type** | **Description**   |
| --------- | -------- | ----------------- |
| Photo     | FString  | User's photo URL. |

**Code**

```cpp
UPeopleLibrary::Photo(const FString& photo);
```

{% endtab %}
{% endtabs %}

## Custom user property

Each project in devtodev can have up to 30 custom user properties.Here is how you can set properties on the current user profile:

{% hint style="warning" %}
Attention! We strongly recommend that you do not use these properties to transfer and store data that fits the definition of [personal data](https://gdpr-info.eu/issues/personal-data/)!
{% endhint %}

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
 * Set properties on a user data.
 *
 * ### Usage:
 *     [DevToDev.activeUser setUserDataWithKey:@"Hair color" andValue: @"copper red"];
 *  properties can have string, integer, date or list type
 *
 * @param NSString key - the name of the property
 * @param {*} value -  a value to set on the given property
 */
[DevToDev.activeUser setUserDataWithKey: (NSString *) key andValue: (id) value];

/**
 * Set multiple properties at once
 *
 * ### Usage:
 *     [DevToDev.activeUser setUserData:@{
 *         @"Hair color"   : @"blonde",
 *           @"Last payment" : 100,
 *           @"Last order"   : @[ 
 *             @"Coloring",
 *             @"Hair Straightening"
 *         ],
 *         @"Order date"   : [NSDate date]
 *     }];
 *  properties can have string, integer, date or list type
 *
 * @param NSDictionary userData - an associative array of names and values.
 */
[DevToDev.activeUser setUserData: (NSDictionary *) userData];
```

{% endtab %}

{% tab title="Android" %}

```java
/**
 * Set properties on a user data.
 *
 * ### Usage:
 *     DevToDev.getActivePlayer().setUserData("Hair color", "copper red");
 *  properties can have String, Number or Collection type
 *
 * @param String key - the name of the property
 * @param {*} value -  a value to set on the given property
 */
DevToDev.getActivePlayer().setUserData(String key, Object value);

/**
 * Set multiple properties at once
 *
 * ### Usage:
 *     final Map<String, Object> userData = new HashMap<String, Object>();
 *     userData.put("Hair color", "blonde");
 *     userData.put("Last payment", 100);
 *     final List<Object> lastOrder = new ArrayList<Object>();
 *     lastOrder.add("Coloring");
 *     lastOrder.add("Hair Straightening");
 *     userData.put("Last order", lastOrder);
 *     DevToDev.getActivePlayer().setUserData(userData);
 *  properties can have String, Number or Collection type
 *
 * @param Map<String, Object> userData - an associative array of names and values.
 */
DevToDev.getActivePlayer().setUserData(final Map<String, Object> userData);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
 * Set properties on a user data.
 *
 * ### Usage:
 *     DevToDev.SDK.ActiveUser.SetUserData("Hair color", "copper red");
 *  properties can have string, int, double or List<object> type
 *
 * <param name="key">property name</param>
 * <param name="value">a value to set on the given property</param>
 */
DevToDev.SDK.ActiveUser.SetUserData(string key, object value);

/**
 * Set multiple properties at once
 *
 * ### Usage:
 *     Dictionary<string, object> userData = new Dictionary<string, object>();
 *     userData.Add("Hair color", "blonde");
 *       userData.Add("Last payment", 100);
 *     List<object> lastOrder = List<object>();
 *     lastOrder.Add("Coloring");
 *     lastOrder.Add("Hair Straightening");
 *     userData.Add("Last order", lastOrder);
 *     DevToDev.SDK.ActiveUser.SetUserData(userData);
 *  properties can have string, int, double or List<object> type
 *
 * <param name="userData">an associative array of names and values</param>
 */
DevToDev.SDK.ActiveUser.SetUserData(Dictionary<string, object> userData);
```

{% endtab %}

{% tab title="Web" %}

```javascript
/*
 * Set properties on a user data.
 *
 * ### Usage:
 *     devtodev.user.set('Hair color', 'copper red');
 *
 *     // to set multiple properties at once
 *     devtodev.user.set({
 *         'Hair color': 'blonde',
 *            'Last payment': 100,
 *            'Last order': ['Coloring','Hair Straightening'],
 *         'Order date': new Date()
 *     });
 *     // properties can be strings, integers, dates, or lists
 *
 * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values.
 * @param {*} [val] A value to set on the given property
 */
devtodev.user.set(prop, val);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <summary> Set properties on a user data. </summary>
/// <example> Usage:
/// 
///     DevToDev.Analytics.ActiveUser.SetUserData("Hair color", "copper red");
///     //properties can have string, int, double or List<object> type
/// 
/// </example>
/// <param name="key">property name</param>
/// <param name="value">a value to set on the given property</param>
DevToDev.Analytics.ActiveUser.SetUserData(string key, object value);

/// <summary> Set multiple properties at once. </summary>
/// <example> Usage:
/// 
///     Dictionary<string, object> userData = new Dictionary<string, object>();
///     userData.Add("Hair color", "blonde");
///     userData.Add("Last payment", 100);
///     List<object> lastOrder = List<object>();
///     lastOrder.Add("Coloring");
///     lastOrder.Add("Hair Straightening");
///     userData.Add("Last order", lastOrder);
///     DevToDev.Analytics.ActiveUser.SetUserData(userData);
///     //properties can have string, int, double or List<object> type
/// 
/// </example>
/// <param name="userData">an associative array of names and values</param>
DevToDev.Analytics.ActiveUser.SetUserData(Dictionary<string, object> userData);
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
 * Set properties on a user data.
 *
 * ### Usage:
 *     [DevToDev.activeUser setUserDataWithKey:@"Hair color" andValue: @"copper red"];
 *  properties can have string, integer, date or list type
 *
 * @param NSString key - the name of the property
 * @param {*} value -  a value to set on the given property
 */
[DevToDev.activeUser setUserDataWithKey: (NSString *) key andValue: (id) value];

/**
 * Set multiple properties at once
 *
 * ### Usage:
 *     [DevToDev.activeUser setUserData:@{
 *         @"Hair color"   : @"blonde",
 *           @"Last payment" : 100,
 *           @"Last order"   : @[ 
 *             @"Coloring",
 *             @"Hair Straightening"
 *         ],
 *         @"Order date"   : [NSDate date]
 *     }];
 *  properties can have string, integer, date or list type
 *
 * @param NSDictionary userData - an associative array of names and values.
 */
[DevToDev.activeUser setUserData: (NSDictionary *) userData];
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
 * Set properties on a user data.
 * ### Usage:
 *     DevToDev.getActiveUser().SetUserData("Hair color", "copper red");
 *  properties can have String, Number or Array type
 *
 * @param key - the name of the property
 * @param value -  a value to set on the given property
 */
DevToDev.getActiveUser().SetUserData(key:String, value:Object);

/**
 * Set multiple properties at once
 *
 * ### Usage:
 *     var userData:Dictionary = new Dictionary();
 *     userData["Hair color"] = "blonde";
 *       userData["Last payment"] = 100;
 *     var lastOrder:Array = new Array();
 *     lastOrder.push("Coloring");
 *     lastOrder.push("Hair Straightening");
 *     userData["Last order"] = lastOrder;
 *     DevToDev.getActiveUser().SetUserDataMany(userData);
 *  properties can have String, Number or Collection type
 *
 * @param userData - an associative array of names and values.
 */
DevToDev.getActiveUser().SetUserDataMany(userData:Dictionary);
```

{% endtab %}

{% tab title="UE4" %}
**Blueprint**

![](/files/-LnlSivfxUvHdGU-tp1Q)

| Field      | Type                         | Description                                                                                          |
| ---------- | ---------------------------- | ---------------------------------------------------------------------------------------------------- |
| Attributes | TArray\<FAnalyticsEventAttr> | Key-value array to set custom property, where key is a user property name, value is a property value |

**Code**

```cpp
UPeopleLibrary::SetUserData(const TArray<FAnalyticsEventAttr>& Attributes);
```

{% endtab %}
{% endtabs %}

## Increment of the custom property

Increments the given numeric properties by the given values.

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
 * Increments or decrements numeric user's properties.
 * ### Usage:
 *     [DevToDev.activeUser incrementWithKey: @"Rounds played" andValue: 1];
 *
 *     // to decrement a counter, pass a negative number
 *     [DevToDev.activeUser incrementWithKey: @"Rounds played" andValue: -1];
 *
 * @param NSString key - the name of the property
 * @param NSNumber value - an amount to increment the given property
 */
[DevToDev.activeUser incrementWithKey: (NSString *) key andValue: (id) value];

/**
 * Increments or decrements multiple numeric user's properties at once.
 *  ### Usage:
 *     [DevToDev.activeUser increment: @{
 *         @"Rounds played"  : 1,
 *         @"Enemies killed" : 6
 *     }];
 *
 * @param NSDictionary values - an associative array of property names and numeric values.
 */
[DevToDev.activeUser increment: (NSDictionary *) values];
```

{% endtab %}

{% tab title="Android" %}

```java
/**
 * Increments or decrements numeric user's properties.
 * ### Usage:
 *     DevToDev.getActivePlayer().increment("Rounds played", 1);
 *
 *     // to decrement a counter, pass a negative number
 *     DevToDev.getActivePlayer().increment("Rounds played", -1);
 *
 * @param String key - the name of the property
 * @param Number value - an amount to increment the given property
 */
DevToDev.getActivePlayer().increment(String key, Number value);

/**
 * Increments or decrements multiple numeric user's properties at once.
 *  ### Usage:
 *     final Map<String, Number> data = new HashMap<String, Number>();
 *     data.put("Rounds played", 1);
 *     data.put("Enemies killed", 6);
 *     DevToDev.getActivePlayer().increment(data);
 *
 * @param Map<String, Number> values - an associative array of property names and numeric values.
 */
DevToDev.getActivePlayer().increment(final Map<String, Number> data);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
 * Increments or decrements numeric user's properties.
 * ### Usage:
 *     DevToDev.SDK.ActiveUser.Increment("Rounds played", 1);
 *
 *     // to decrement a counter, pass a negative number
 *     DevToDev.SDK.ActiveUser.Increment("Rounds played", -1);
 *
 * <param name="key">property name</param>
 * <param name="value">an amount to increment the given property(int or double)</param>
 */
DevToDev.SDK.ActiveUser.Increment(string key, object value);

/**
 * Increments or decrements multiple numeric user's properties at once.
 *  ### Usage:
 *     Dictionary<string, object> data = new Dictionary<string, object>();
 *     data.Add("Rounds played", 1);
 *     data.Add("Enemies killed", 6);
 *     DevToDev.SDK.ActiveUser.Increment(data);
 *
 * <param name="values">an associative array of property names and numeric(int, double) values</param>
 */
DevToDev.SDK.ActiveUser.Increment(Dictionary<string, object>  values);
```

{% endtab %}

{% tab title="Web" %}

```javascript
/*
 * Increments or decrements numeric user's properties.
 * ### Usage:
 *     devtodev.user.increment('Rounds played', 1);
 *     // or if you're just incrementing a counter by 1, you can simply do
 *     devtodev.user.increment('Rounds played');
 *
 *     // to decrement a counter, pass a negative number
 *     devtodev.user.increment('Rounds played', -1);
 *
 *     // you can increment multiple properties at once:
 *     devtodev.user.increment({
 *         'Rounds played': 1,
 *         'Enemies killed': 6
 *     });
 * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and numeric values.
 * @param {Number} [val] An amount to increment the given property
 */
devtodev.user.increment(prop, val);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <summary> Increments or decrements numeric user's properties.</summary>
/// <example> Usage:
/// 
///     DevToDev.Analytics.ActiveUser.Increment("Rounds played", 1);
///     //to decrement a counter, pass a negative number
///     DevToDev.Analytics.ActiveUser.Increment("Rounds played", -1);
/// 
/// </example>
/// <param name="key">property name</param>
/// <param name="value">an amount to increment the given property(int or double)</param>
DevToDev.Analytics.ActiveUser.Increment(string key, object value);

/// <summary> Increments or decrements multiple numeric user's properties at once.</summary>
/// <example> Usage:
/// 
///     Dictionary<string, object> data = new Dictionary<string, object>();
///     data.Add("Rounds played", 1);
///     data.Add("Enemies killed", 6);
///     DevToDev.Analytics.ActiveUser.Increment(data);
/// 
/// </example>
/// <param name="values">an associative array of property names and numeric(int, double) values</param>
DevToDev.Analytics.ActiveUser.Increment(Dictionary<string, object>  values);
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
 * Increments or decrements numeric user's properties.
 * ### Usage:
 *     [DevToDev.activeUser incrementWithKey: @"Rounds played" andValue: 1];
 *
 *     // to decrement a counter, pass a negative number
 *     [DevToDev.activeUser incrementWithKey: @"Rounds played" andValue: -1];
 *
 * @param NSString key - the name of the property
 * @param NSNumber value - an amount to increment the given property
 */
[DevToDev.activeUser incrementWithKey: (NSString *) key andValue: (id) value];

/**
 * Increments or decrements multiple numeric user's properties at once.
 *  ### Usage:
 *     [DevToDev.activeUser increment: @{
 *         @"Rounds played"  : 1,
 *         @"Enemies killed" : 6
 *     }];
 *
 * @param NSDictionary values - an associative array of property names and numeric values.
 */
[DevToDev.activeUser increment: (NSDictionary *) values];
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
 * Increments or decrements numeric user's properties.
 * ### Usage:
 *     DevToDev.getActiveUser().Increment("Rounds played", 1);
 *
 *     // to decrement a counter, pass a negative number
 *     DevToDev.getActiveUser().Increment("Rounds played", -1);
 *
 * @param key - the name of the property
 * @param value - an amount to increment the given property
 */
DevToDev.getActiveUser().Increment(key:String, value:Number);

/**
 * Increments or decrements multiple numeric user's properties at once.
 *  ### Usage:
 *     var data:Dictionary = new Dictionary();
 *     data["Rounds played"] = 1;
 *     data["Enemies killed"] 6;
 *     DevToDev.getActivePlayer().IncrementMany(data);
 *
 * @param values - an associative array of property names and numeric values.
 */
DevToDev.getActiveUser().IncrementMany(data:Dictionary);
```

{% endtab %}

{% tab title="UE4" %}
**Blueprint**

![](/files/-LnlSivjP3NrWx33-sl3)

| Field      | Type                         | Description                                                                 |
| ---------- | ---------------------------- | --------------------------------------------------------------------------- |
| Attributes | TArray\<FAnalyticsEventAttr> | Key-value array, where key is a user property name, value is increment step |

**Code**

```cpp
UPeopleLibrary::IncrementUserData(const TArray<FAnalyticsEventAttr>& Attributes);
```

{% endtab %}
{% endtabs %}

## Append to custom property (deprecated)

Adds values to a list-valued property. If the property does not currently exist, it will be created with the given list as it's value. If the property exists and is not list-valued, the append will be ignored.

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* Append values to list properties.
* @param NSString key - property name
* @param {NSString|NSNumber|NSNull|NSDictionary|NSDate|NSURL} value - appending value
*/
[DevToDev.activeUser appendWithKey: (NSString *) key andValue: (id) value];

/**
 * Multiple append list-valued properties at once
 * @param NSDictionary values - an associative array of property names and values.
 */
[DevToDev.activeUser append: (NSDictionary *) values];
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* Append values to list properties.
* @param String key - property name
* @param {String|Number|null|Collection|Map|Date|Url} value - appending value
*/
DevToDev.getActivePlayer().append(String key, Object value);

/**
 * Multiple append list-valued properties at once
 * @param Map<String, Object> values - an associative array of property names and values.
 */
DevToDev.getActivePlayer().append(final Map<String, Object> data);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
* Append values to list properties.
* <param name="key">property name</param>
* <param name="value">appending value of type {string|int|double|List}</param>
*/
DevToDev.SDK.ActiveUser.Append(string key, object value);

/**
 * Multiple append list-valued properties at once
 * <param name="values">an associative array of property names and values</param>
 */
DevToDev.SDK.ActiveUser.Append(Dictionary<string, object> values);
```

{% endtab %}

{% tab title="Web" %}

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <summary> Append values to list properties. </summary>
/// <param name="key"> Property name </param>
/// <param name="value"> Appending value of type {string|int|double|List} </param>
DevToDev.Analytics.ActiveUser.AppendUserData(string key, object value);

/// <summary> Multiple append list-valued properties at once </summary>
/// <param name="values"> An associative array of property names and values </param>
DevToDev.Analytics.ActiveUser.AppendUserData(Dictionary<string, object> values);
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* Append values to list properties.
* @param NSString key - property name
* @param {NSString|NSNumber|NSNull|NSArray|NSDictionary|NSDate|NSURL} value - appending value
*/
[DevToDev.activeUser appendWithKey: (NSString *) key andValue: (id) value];

/**
 * Multiple append list-valued properties at once
 * @param NSDictionary values - an associative array of property names and values.
 */
[DevToDev.activeUser append: (NSDictionary *) values];
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* Append values to list properties.
* @param key - property name
* @param value - appending value (String|Number|Array)
*/
DevToDev.getActiveUser().AppendUserData(key:String, value:Object);

/**
 * Multiple append list-valued properties at once
 * @param values - an associative array of property names and values.
 */
DevToDev.getActiveUser().AppendUserDataMany(data:Dictionary);
```

{% endtab %}

{% tab title="UE4" %}

{% endtab %}
{% endtabs %}

## Union with custom property (deprecated)

Adds values to a list-valued property only if they are not already present in the list. If the property does not currently exist, it will be created with the given list as it's value. If the property exists and is not list-valued, the union will be ignored.

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
* Union a given list with a list-valued property, excluding duplicate values.
* @param NSString key - property name
* @param {NSString|NSNumber|NSNull|NSDictionary|NSDate|NSURL} value - appending value
*/
[DevToDev.activeUser unionWithKey: (NSString *) key andValue: (id) value];

/**
 * Multiple union of a given lists with a list-valued properties at once
 * @param NSDictionary values - an associative array of property names and values.
 */
[DevToDev.activeUser unionWithData: (NSDictionary *) values];
```

{% endtab %}

{% tab title="Android" %}

```java
/**
* Union a given list with a list-valued property, excluding duplicate values.
* @param String key - property name
* @param {String|Number|null|Collection|Map|Date|Url} value - appending value
*/
DevToDev.getActivePlayer().union(String key, Object value);

/**
 * Multiple union of a given lists with a list-valued properties at once
 * @param Map values - an associative array of property names and values.
 */
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
* Union a given list with a list-valued property, excluding duplicate values.
* <param name="key">property name</param>
* <param name="value">appending value of type {string|int|double|List}</param>
*/
DevToDev.SDK.ActiveUser.Union(string key, object value);

/**
 * Multiple union of a given lists with a list-valued properties at once
 * <param name="values">an associative array of property names and values</param>
 */
DevToDev.SDK.ActiveUser.Union(Dictionary<string, object> values);
```

{% endtab %}

{% tab title="Web" %}

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <summary> Union a given list with a list-valued property, excluding duplicate values. </summary>
/// <param name="key"> Property name </param>
/// <param name="value"> Appending value of type {string|int|double|List} </param>
DevToDev.Analytics.ActiveUser.AppendUserData(string key, object value);

/// <summary> Multiple union of a given lists with a list-valued properties at once </summary>
/// <param name="values"> An associative array of property names and values </param>
DevToDev.Analytics.ActiveUser.AppendUserData(Dictionary<string, object> values);
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
* Union a given list with a list-valued property, excluding duplicate values.
* @param NSString key - property name
* @param {NSString|NSNumber|NSNull|NSArray|NSDictionary|NSDate|NSURL} value - appending value
*/
[DevToDev.activeUser unionWithKey: (NSString *) key andValue: (id) value];

/**
 * Multiple union of a given lists with a list-valued properties at once
 * @param NSDictionary values - an associative array of property names and values.
 */
[DevToDev.activeUser unionWithData: (NSDictionary *) values];
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
* Union a given list with a list-valued property, excluding duplicate values.
* @param key - property name
* @param value - appending value (String|Number|Array)
*/
DevToDev.getActiveUser().UnionUserData(key:String, value:Object);

/**
 * Multiple union of a given lists with a list-valued properties at once
 * @param values - an associative array of property names and values.
 */
DevToDev.getActiveUser().UnionUserDataMany(data:Dictionary);
```

{% endtab %}

{% tab title="UE4" %}

{% endtab %}
{% endtabs %}

## Removing of the custom property

Removes a property or a list of properties and their values from the current user's profile.

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
 * Removes property from user data.
 *
 * ### Usage:
 *     [DevToDev.activeUser unsetUserDataWithKey: @"Hair color"];
 *
 * @param NSString key - the name of the property
 */
[DevToDev.activeUser unsetUserDataWithKey: (NSString *) key];

/**
 * Removes multiple properties from user data at once.
 *
 * ### Usage:
 *     [DevToDev.activeUser unsetUserData: @[ @"Hair color", @"blonde", @"Last payment" ]];
 *
 * @param NSArray keys - an array of property names.
 */
[DevToDev.activeUser unsetUserData: (NSArray *) keys];
```

{% endtab %}

{% tab title="Android" %}

```java
/**
 * Removes property from user data.
 *
 * ### Usage:
 *     DevToDev.getActivePlayer().unsetUserData("Hair color");
 *
 * @param String key - the name of the property
 */
DevToDev.getActivePlayer().unsetUserData(String key);

/**
 * Removes multiple properties from user data at once.
 *
 * ### Usage:
 *     final List<String> unsetData = new ArrayList<String>();
 *     unsetData.add("Hair color");
 *     unsetData.add("Last payment");
 *     DevToDev.getActivePlayer().unsetUserData(unsetData);
 *
 * @param List keys - an array of property names.
 */
DevToDev.getActivePlayer().unsetUserData(final List<String> keys);
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
 * Removes property from user data.
 *
 * ### Usage:
 *     DevToDev.SDK.ActiveUser.UnsetUserData("Hair color");
 *
 * <param name="key">the name of the property</param>
 */
DevToDev.SDK.ActiveUser.UnsetUserData(string key);

/**
 * Removes multiple properties from user data at once.
 *
 * ### Usage:
 *     List<string> unsetData = new List<string>();
 *     unsetData.add("Hair color");
 *     unsetData.add("Last payment");
 *     DevToDev.SDK.ActiveUser.UnsetUserData(unsetData);
 *
 * <param name="keys">an array of property names</param>
 */
DevToDev.SDK.ActiveUser.UnsetUserData(List<string> keys);
```

{% endtab %}

{% tab title="Web" %}

```javascript
/*
 * Removes properties from a user data.
 *
 * ### Usage:
 *     devtodev.user.remove('Hair color');
 *
 *     // to set multiple properties at once
 *     devtodev.user.remove(['Hair color', 'blonde', 'Last payment']);
 *
 * @param {Array|String} prop If a string, this is the name of the property. If an array, this is an array of names.
 */
devtodev.user.remove(prop);
```

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <summary> Removes property from user data. </summary>
/// <example> Usage:
/// 
///     DevToDev.Analytics.ActiveUser.UnsetUserData("Hair color");
/// 
/// </example>
/// <param name="key"> The name of the property </param>
DevToDev.Analytics.ActiveUser.UnsetUserData(string key);

/// <summary> Removes multiple properties from user data at once. </summary>
/// <example> Usage:
/// 
///     List<string> unsetData = new List<string>();
///     unsetData.add("Hair color");
///     unsetData.add("Last payment");
///     DevToDev.Analytics.ActiveUser.UnsetUserData(unsetData);
/// 
/// </example>
/// <param name="keys"> An array of property names </param>
DevToDev.Analytics.ActiveUser.UnsetUserData(List<string> keys);
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
 * Removes property from user data.
 *
 * ### Usage:
 *     [DevToDev.activeUser unsetUserDataWithKey: @"Hair color"];
 *
 * @param NSString key - the name of the property
 */
[DevToDev.activeUser unsetUserDataWithKey: (NSString *) key];

/**
 * Removes multiple properties from user data at once.
 *
 * ### Usage:
 *     [DevToDev.activeUser unsetUserData: @[ @"Hair color", @"blonde", @"Last payment" ]];
 *
 * @param NSArray keys - an array of property names.
 */
[DevToDev.activeUser unsetUserData: (NSArray *) keys];
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
 * Removes property from user data.
 * ### Usage:
 *     DevToDev.getActiveUser().UnsetUserData("Hair color");
 *
 * @param key - the name of the property
 */
DevToDev.getActiveUser().UnsetUserData(key:String);

/**
 * Removes multiple properties from user data at once.
 * ### Usage:
 *     var unsetData:Array = new Array();
 *     unsetData.push("Hair color");
 *     unsetData.push("Last payment");
 *     DevToDev.getActiveUser().UnsetUserDataMany(unsetData);
 *
 * @param keys - an array of property names.
 */
DevToDev.getActiveUser().UnsetUserDataMany(keys:Array);
```

{% endtab %}

{% tab title="UE4" %}
**Blueprint**

![](/files/-LnlSivpFU2txQbkhig7)

| Field      | Type             | Description                               |
| ---------- | ---------------- | ----------------------------------------- |
| Attributes | TArray\<FString> | An array of property names to be removed. |

**Code**

```cpp
UPeopleLibrary::UnsetUserData(const TArray<FString>& Attributes);
```

{% endtab %}
{% endtabs %}

## Clearing of the all custom properties

{% tabs %}
{% tab title="iOS" %}

```objectivec
/**
 * Removes all user's custom personal data from devtodev data base.
 */
[DevToDev.activeUser clearUserData];
```

{% endtab %}

{% tab title="Android" %}

```java
/**
 * Removes all user's custom personal data from devtodev data base.
 */
DevToDev.getActivePlayer().clearUserData();
```

{% endtab %}

{% tab title="Windows 8.1 and 10" %}

```csharp
/**
 * Removes all user's custom personal data from devtodev data base.
 */
DevToDev.SDK.ActiveUser.ClearUserData();
```

{% endtab %}

{% tab title="Web" %}

```javascript
/*
 * Removes all user's custom personal data from devtodev data base.
 */

devtodev.user.clearUser();
```

{% endtab %}

{% tab title="Unity" %}

```csharp
/// <summary> Removes all user's custom personal data from devtodev data base.</summary>
DevToDev.Analytics.ActiveUser.ClearUserData();
```

{% endtab %}

{% tab title="Mac OS" %}

```objectivec
/**
 * Removes all user's custom personal data from devtodev data base.
 */
[DevToDev.activeUser clearUserData];
```

{% endtab %}

{% tab title="Adobe Air" %}

```javascript
/**
 * Removes all user's custom personal data from devtodev data base.
 */
DevToDev.getActiveUser().ClearUserData();
```

{% endtab %}

{% tab title="UE4" %}
**Blueprint**

![](/files/-LnlSivsteoZnAhyQce9)

**Code**

```cpp
UPeopleLibrary::ClearUserData();
```

{% endtab %}
{% endtabs %}


# Anti-cheat Methods

{% hint style="danger" %}
**This generation of SDK is deprecated and is no longer supported.**\
Information about the [current version can be found here](/integration/integration-of-sdk-v2/setting-up-events/anticheat-methods).
{% endhint %}

## Validation of payments and time adjustments in devtodev SDK for iOS

### **Payments validation**

To be protected from fraudulent transactions, we recommend you to use devtodev Anticheat service.

Use this method, and devtodev will check the transaction's validity with the payment platform, and the response will be returned to the application.

```
[DevToDevCheat verifyPaymentWithCompletion:(void (^)(ReceiptStatus))completionBlock];
```

The result can take one of the following values:

```
typedef enum {
    ReceiptValid,
    ReceiptNotValid,
    ReceiptServerError,
    ReceiptInternalError,
    ReceiptSandbox
} ReceiptStatus;
```

In case of a successful check call the following main SDK method:

```
[DevToDev realPayment: (NSString *) transactionId withInAppPrice:(float) inAppPrice 
         andInAppName: (NSString *) inAppName andInAppCurrencyISOCode: (NSString *) inAppCurrencyISOCode];
```

If the transaction hasn’t passed verification, do not perform the Payment event.

{% hint style="warning" %}
We do not recommend to use the result of devtodev anti-cheat verification as a condition for giving or not giving in-game currency or item purchased by a user!
{% endhint %}

### **Time cheats check**

To check for time cheats call checkTime method every time when the app is being launched

```
[DevToDevCheat checkTime: (void (^)(TimeStatus status)) completionBlock];
```

The result can take one of the following values:

```
typedef enum {
    Valid,
    Forward,
    Rewind
} TimeStatus;
```

## Validation of payments and time adjustments in devtodev SDK for Android

### Payments validation

To be protected from fraudulent transactions, we recommend you to use devtodev Anticheat service

Use this method, and devtodev will check the transaction validity with the payment platform, and the response will be returned to the application.

Call following method when GooglePlay returns the transaction to your onActivityResult:

```
DevToDevCheat.verifyPayment(String receipt, String signature, String publicKey, 
                            OnVerifyListener onVerifyListener);
```

You can get sharedSecret key here:

1. Go to the Google Play Developer Console and sign in. Make sure that you sign in to the account from which the application you are licensing is published (or will be published).
2. In the application details page, locate the Services & APIs link and click it.
3. In the Services & APIs page, locate the Licensing & In-App Billing section.

Your public key for licensing is given in the Your License Key For This Application field.

The result can take one of the following values:

```
public enum VerifyStatus {	
                           Valid,
                           Invalid,
                           InternalError,
                           ServerError
                         };
```

In case of a successful check call following the main SDK method:

```
DevToDev.realPayment(String pPaymentId, float pInAppPrice, String pInAppName, String pInAppCurrencyISOCode);
```

If the transaction hasn’t passed verification, do not perform the Payment event.

{% hint style="warning" %}
We do not recommend to use the result of devtodev anti-cheat verification as a condition for giving or not giving in-game currency or item purchased by a user!
{% endhint %}

### Time cheats check

To check for time cheats call checkTime method every time when the app is being launched

```
DevToDevCheat.verifyTime(OnTimeVerifyListener onTimeVerifyListener);
```

The result can take one of the following values:

```
public enum TimeStatus {
                         Valid,
                         Forward,
                         Rewind
                       };
```

## Validation of payments and time adjustments in devtodev SDK for Unity

### Payments validation

To be protected from fraudulent transactions, we recommend you to use devtodev Anticheat service.

Use this method, and devtodev will check the transaction validity with the payment platform, and the response will be returned to the application.

1\. Call the method for payment verification:

```
DevToDev.AntiCheat.VerifyReceipt(string receipt, string signature, string publicKey,
                                 OnReceiptVerifyCallback callback);
```

or if you are using Unity IAP plugin:

```
DevToDev.AntiCheat.VerifyReceipt(string purchasedProduct, string publicKey, OnReceiptVerifyCallback callback)
```

where OnReceiptVerifyCallback is the function like this:

```
public void onReceiptVerifyCallback (DevToDev.ReceiptVerificationStatus status) {
  Debug.Log ("Verification status" + status);
  //TODO put your source here
}
```

Here's how to find your application's public key for licensing (for Google Play platform only, for other platforms the publicKey is not used):

1. Go to the [Google Play Console](http://play.google.com/apps/publish) and sign in. Make sure that you sign in to the account from which the application you are licensing is published (or will be published).
2. In the application details page, locate the Services & APIs link and click it.
3. In the Services & APIs page, locate the Licensing & In-App Billing section. Your public key for licensing is given in the Your License Key For This Application field.

ReceiptVerificationStatus can take one of the following values:

```
public enum ReceiptVerificationStatus {
  ReceiptValid,
  ReceiptNotValid,
  ReceiptServerError,
  ReceiptSandbox,
  ReceiptInternalError    
};
```

{% hint style="info" %}
**Don't forget that it is enough to set only&#x20;*****receipt*****&#x20;field to check the payment on iOS (iTunes) or Windows/Windows Phone (Microsoft Store), and for Android (Google Play) the fields&#x20;*****signature*****&#x20;and&#x20;*****publicKey*****&#x20;should be set.**
{% endhint %}

{% hint style="warning" %}
Сore SDK should be initialized prior to the call of **VerifyPayment** function. &#x20;
{% endhint %}

2\. In case of an unsuccessful check (ReceiptNotValid result) do not call SDK method RealPayment. In other cases:&#x20;

```
DevToDev.Anatylics.RealPayment(string pPaymentId, float pInAppPrice, string pInAppName,
                               string pInAppCurrencyISOCode);
```

### Time cheats check

To check for time cheats call VerifyTime method.

1\. Call the method to time verification:

```
DevToDev.AntiCheat.VerifyTime(OnTimeVerifyCallback callback);
```

where OnTimeVerifyCallback is the function like this:

```
public void onTimeVerifyFinished (DevToDev.TimeVerificationStatus status) {
  Debug.Log ("Verification status" + status);
  //TODO put your source here
};
```

DevToDevTimeVerificationStatus can take one of the following values:

```
public enum TimeVerificationStatus {
   TimeValid,
   TimeForward,
   TimeRewind
};
```

{% hint style="warning" %}
Сore SDK should be initialized prior to the call of VerifyTime function.&#x20;
{% endhint %}


# Push Notifications


# IOS

Integration with iOS push notification service

{% hint style="danger" %}
**This generation of SDK is deprecated and is no longer supported.**\
Information about the [current version can be found here](/integration/integration-of-sdk-v2/push-notifications/ios).
{% endhint %}

## General information

To enable Push Notifications, please perform the following actions:

1. Add the application to your space in devtodev system.
2. Generate Developer or Production Certificate for the application and get Private key file (.p12) on its basis.
3. Submit the data to the application settings in devtodev system.
4. Integrate devtodev SDK to the application (see the [SDK integration](/integration/integration-of-sdk/analytics-integration) section to learn more about integrating and initializing devtodev SDK).
5. Add several lines of the code to switch in the push notification to the SDK.
6. Create a campaign for sending push notifications in [Push Notifications](/reports-and-functionality/project-related-reports-and-fuctionality/experiments/push-notifications) section.

## Enabling Push Notifications and certificate generation

### Enabling Push Notifications

First enable push notifications in your Xcode project.

The library provides support for iOS 10 notification attachments, such as images, animated gifs, and video. In order to take advantage of this functionality, you will need to create a notification service extension alongside your main application.

Create a new iOS target in Xcode (File -> New -> Target) and select the Notification Service Extension type.

​In Member Center, the Push Notifications service will appear as Configurable (not Enabled) until you create a client SSL certificate.

![](https://www.devtodev.com/upload/images/3%2814%29.png)

Drag the devtodevAppExtensions.framework into your app project.

![](https://www.devtodev.com/upload/images/11%284%29.png)

Modify your extension.

1. Delete all dummy source code for your new extension
2. Inherit from DTDMediaAttachmentExtension in NotificationService

```
//NotificationService.h
#import <devtodevAppExtensions/devtodevAppExtensions.h>

@interface NotificationService : DTDMediaAttachmentExtension

@end

//NotificationService.m
#import "NotificationService.h"

@implementation NotificationService
// NOTE: Keep this empty implementation to prevent class stripping.
@end
```

#### **Adding Capabilities**

Use Xcode to enable push notifications in the target’s Capabilities pane:

![](https://www.devtodev.com/upload/images/8%285%29.png)

Enable Background Modes and Remote notifications under the target’s Capabilities section:

![](https://www.devtodev.com/upload/images/7%287%29.png)

#### Creating a Universal Push Notification Client SSL Certificate

You use Member Center to generate a push notification client SSL certificate that allows your notification server to connect to the APNs. Each App ID is required to have its own client SSL certificate. The client SSL certificate Member Center generates is a universal certificate that allows your app to connect to both the development and production environments.

{% hint style="info" %}
Only a team agent or admin can generate Apple Push Notification service SSL certificates.
{% endhint %}

To generate a universal client SSL certificate

1. In [Certificates, Identifiers & Profiles](http://developer.apple.com/account), select Certificates.
2. Click the Add button (+) in the upper-right corner.\
   ![](https://www.devtodev.com/upload/images/12_ios_apns_certificate_1_2x.png)
3. Under Production, select the “Apple Push Notification service SSL (Sandbox & Production)” checkbox, and click Continue.\
   ![](https://www.devtodev.com/upload/images/12_ios_apns_certificate_2_2x.png)
4. Choose an App ID from the App ID pop-up menu, and click Continue. Choose the explicit App ID that matches your bundle ID.
5. Follow the instructions on the next webpage to create a certificate request on your Mac, and click Continue.
6. Click Choose File.
7. In the dialog that appears, select the certificate request file (with a .certSigningRequest extension), and click Choose.\
   ![](https://www.devtodev.com/upload/images/12_ios_apns_certificate_3_2x.png)
8. Click Generate.
9. Click Download.

#### **Follow these steps to export the certificate from Apple web-site to the P12-file:**

1. Open "Keychain access" application
2. If the certificate hasn't been added to keychain access yet, choose "File" →  "Import". Find the certificate file (CER-file) provided by Apple
3. Choose "Keys" section in "Keychain access" application
4. Choose a personal key associated with your iPhone developer certificate. Personal key is identified by open certificate associated with it "iPhone developer: ". Choose "File" → Export objects. Save key as .p12
5. You'll be suggested to create a password which is used when you need to import the key to another computer

#### **Convert  the certificate from Apple web-site to the P12-file on Windows OS**

Convert Apple certificate file to the PEM-file. Start following command-line operation from bin catalog OpenSSL.

```
openssl x509 -in developer_identity.cer -inform DER -out developer_identity.pem -outform PEM
```

Convert personal key from Mac OS keychain to the PEM-key:

```
openssl pkcs12 -nocerts -in mykey.p12 -out mykey.pem
```

Now you are able to create P12-file using PEM-key and iPhone developer certificate:

```
openssl pkcs12 -export -inkey mykey.key -in developer_identity.pem -out iphone_dev.p12
```

If you are using key from Mac OS keychain then choose PEM-version created in the previous step.\
Otherwise, you can use OpenSSL key for Windows OS.

### Upload the certificate to the site

Upload the .p12-file into Integration section of application settings panel  (Settings -> Push Notifications):

![](/files/-M4OUZotk6PsJCCGiFhB)

## SDK Integration

After the certificate has been generated you can start to integrate Push SDK into your app.

1\. Open the "Capabilities" tab in the XCode.  Switch "Push Notifications" and "Background Modes" to ON. In "Background Modes" check "Remote notifications".

2\. Add the following strings to the AppDelegate class:

```
#import "AppDelegate.h"
#import <devtodev/devtodev.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary
*)launchOptions {
    
   [DevToDev initWithKey:@“APP_KEY” andSecretKey:@“SECRET_KEY”];

    dispatch_async(dispatch_get_main_queue(), ^{
      [DevToDev pushManager].delegate = self;
      [DevToDev pushManager].pushNotificationsOptions = (DTDNotificationOptionAlert | DTDNotificationOptionBadge | DTDNotificationOptionSound);
      [DevToDev pushManager].pushNotificationsEnabled = YES;
    });

    // Your code...
    return YES;
}


/**
 * @brief Device received a token and was successfully subscribed for notifications
 * @param deviceToken device push token
 **/
-(void) didRegisterForRemoteNotificationsWithDeviceToken: (NSString *) deviceToken {

}

/**
 * @brief Error occured while registering for push notifications
 * @param error text
 **/
-(void) didFailToRegisterForRemoteNotificationsWithError: (NSError *) error {

}

/**
 * @brief Push notification has been received by the device
 * @param notification data
 **/
-(void) didReceiveRemoteNotification: (NSDictionary *) notification {

}

/**
 * @brief Push notification has been opened
 * @param pushMessage body
 * @param actionButton button that was clicked
 **/
-(void) didOpenRemoteNotification: (DTDPushMessage *) pushMessage withAction: (DTDActionButton *) actionButton {

}

/**
 * @brief Push notification receive response
 * Note: This method is relevant only for iOS 10 and above.
 * @param response notification
 **/
-(void) didReceiveNotificationResponse: (DTDNotificationResponse *)response {

}
```

If you are using iOS 12 or above, you can specify custom options for notification settings:&#x20;

```
typedef NS_OPTIONS(NSUInteger, DTDNotificationOptions) {
    DTDNotificationOptionBadge   = (1 << 0),
    DTDNotificationOptionSound   = (1 << 1),
    DTDNotificationOptionAlert   = (1 << 2),
    DTDNotificationOptionCarPlay = (1 << 3),
    DTDNotificationOptionCriticalAlert = (1 << 4),
    DTDNotificationOptionProvidesAppNotificationSettings = (1 << 5),
    DTDNotificationOptionProvisional = (1 << 6)
};
```

3\. Open "Build Settings", find the parameter "Other Linker Flags", then add 2 flags: *-ObjC* and *-lc++*

![](/files/-MYovRHLtYt6gzjhVb8C)

4\. Compile and run the app. You will need a device because the simulator does not support push notifications.\
Xcode will automatically choose a new provisioning profile. If an error occurred during the launch make sure that there is a correct profile set in the Code Signing Identity.\
You'll be asked to confirm push notifications. An app will request permission only once, if user confirms it - notifications will be accepted otherwise he won't get any push messages from your app. Users can change it in device settings.

### Creating a new push notification in devtodev interface

1\. Open PUSH NOTIFICATIONS section and click on the "Add new campaign" button

![](/files/-M4OV2lLOu1F103aN-ws)

2\. Fill in the campaign name

{% hint style="info" %}
You can create a campaign only after at least one push token comes from devtodev SDK integrated into your application. Otherwise, the app will not be displayed in the list.
{% endhint %}

![](/files/-M4OXSl6_bvIp7uzCqjY)

3\. Choose a user group to send a message. You can choose an existing segment or create a new one

![](/files/-M4OYMgORidREO2WmZBb)

4\. Enter notification details

![](/files/-M4OYbs-hLaOWBnRZcOz)

5\. Test push notification (or skip this step)

![](/files/-M4OYxPUnYWwj4P5AABg)

6\. Confirm push gateway

![](/files/-M4OZNNwp3hvBFffXDrY)

7\. Schedule the delivery

![](/files/-M4OZP2pshb4ZIoODEbz)

8\. That's it!


# Android

Android Push Notifications

{% hint style="danger" %}
**This generation of SDK is deprecated and is no longer supported.**\
Information about the [current version can be found here](/integration/integration-of-sdk-v2/push-notifications/android).
{% endhint %}

Push Notifications on Android are sent with the help of the FCM service. To work with it, two keys are required: a client and server key. If you have a project, move on to the second part of this article.&#x20;

## Creating a project in Firebase

Add a new project to the [Firebase console](https://console.firebase.google.com/).&#x20;

![](https://www.devtodev.com/upload/images/android_push_doc1.png)

Fill in the name and country of your project.

![](https://www.devtodev.com/upload/images/android_push_doc2.png)

Congratulations, the project has been created! Now you need to indicate the package name of your Android app.

![](https://www.devtodev.com/upload/images/android_push_doc4.png)

![](https://www.devtodev.com/upload/images/%D0%A1%D0%BD%D0%B8%D0%BC%D0%BE%D0%BA%20%D1%8D%D0%BA%D1%80%D0%B0%D0%BD%D0%B0%202018-09-25%20%D0%B2%2013.44.45.png)\
After a successful registration in Firebase, you can receive your keys that you will use in devtodev.&#x20;

## Obtaining the necessary keys

Go to the settings of your Android project.

![](https://www.devtodev.com/upload/images/android_push_doc7.png)

Remember or copy Server key and Sender ID from the Cloud Messaging tab. You will need them to integrate push notifications and create push campaigns.&#x20;

![](https://www.devtodev.com/upload/images/android_push_doc8.png)

## Implementation in the app

Download the generated by google-service.json file and add it to the project.&#x20;

The Google services plugin for [Gradle](https://gradle.org/) loads the google-services.json file that you just downloaded. Modify your build.gradle files to use the plugin.

1. Project-level build.gradle (\<project>/build.gradle):&#x20;

   ```
   buildscript {
     dependencies {
       // Add this line
       classpath 'com.google.gms:google-services:4.3.3'
     }
   }
   ```
2. App-level build.gradle (\<project>/\<app-module>/build.gradle):

   ```
   dependencies {
      ...
      implementation 'com.devtodev:android:1.14.5'
      implementation 'com.google.android.gms:play-services-base:17.1.0'
      implementation 'com.google.firebase:firebase-core:17.2.3'
      implementation 'com.google.firebase:firebase-messaging:20.1.0'
   }
   // Add to the bottom of the file
   apply plugin: 'com.google.gms.google-services'
   ```
3. Finally, press "Sync now" in the bar that appears in the IDE: \
   ![](https://www.gstatic.com/mobilesdk/160330_mobilesdk/images/android_studio_gradle_changed_butterbar@2x.png)
4. In Activity (where the SDK initializes) add the push notifications initialization and the listener of push notifications processing.

   ```
   public class MainActivity extends AppCompatActivity implements PushListener {

       @Override
       protected void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           setContentView(R.layout.activity_main);

           DevToDevPushManager.setPushListener(this);
           DevToDevPushManager.init(getIntent());
       }

       @Override
       public void onRegisteredForPushNotifications(String s) {
           // Insert the code for processing the received token

       }

       @Override
       public void onFailedToRegisteredForPushNotifications(String s) {
           // Insert the code for tracking integration errors
       }

       @Override
       public void onPushNotificationsReceived(Map<String, String> map) {
           // Insert the code to track received notifications
       }

       @Override
       public void onPushNotificationOpened(PushMessage pushMessage, @Nullable ActionButton actionButton) {
           // Insert the code to track opened notification
       }
   }
   ```
5. Optional. To set the custom icons to be shown in push-notification on the SDK part, use the following methods:\
   To set the small icon:

   ```
   DevToDevPushManager.setCustomSmallIcon(int resourceId);
   ```

   To set the default color of the small icon:

   ```
   DevToDevPushManager.setCustomSmallIconColor(int colorHexadecimal)
   ```

   To set the large icon:

   ```
   DevToDevPushManager.setCustomLargeIcon(int resourceId);
   ```

   Use resources of your app. For example, R.drawable.ic\_launcherIcons which set in the push-notification wizard have priority over the icons which set in these methods.

{% hint style="warning" %}
**In case you use several push notifications services or would like to use your own implementation of FirebaseMessagingService**, please add the call of the method for displaying messages sent from the devtodev system:
{% endhint %}

```
DevToDevPushManager.displayPushNotification(Context context, RemoteMessage remoteMessage);
```

For example:

```
public class MyFirebaseMessagingService extends FirebaseMessagingService {
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        Map<String, String> data = remoteMessage.getData();
        if (data != null) {
            if (data.containsKey("_k")) {
                DevToDevPushManager.displayPushNotification(this, remoteMessage);
            } else {
                showNotification(remoteMessage);
            }
        }
    }
}
```

## Changing the application settings in devtodev system

1\. Go to [Firebase console](https://console.firebase.google.com/) and then to your project settings. On the Cloud messaging tab get the Firebase Cloud Messaging token of your project.

![](https://www.devtodev.com/upload/images/6%289%29.png)

2\. Proceed to Settings of your app in devtodev.

![](/files/-M4SRFz2Ydu2T5p4b1Ye)

3\. Go to Integration page and insert the previously received Firebase Cloud Messaging token to the FCM token field in Push notifications section.

![](/files/-M4ST26oGxcbNPkMp3Kz)

4\. If the Firebase Cloud Messaging token is correct, you will see the following result

![](/files/-M4STdF3AeQYCDGAPua2)

## Creating a new push notification in devtodev interface

1. Open PUSH NOTIFICATIONS section and click on 'Add new campaign" button
2. Fill in campaign name, select an app for delivery\*
3. Choose user group to send a message. You can choose existing segment or create a new one
4. Enter notification details
5. Make some tests and correct the message if it's required
6. Schedule the delivery
7. That's it!

{% hint style="warning" %}
You can create a campaign only after at least one push token comes from devtodev SDK integrated to your application. Otherwise the app will not be displayed in the list.
{% endhint %}


# Windows 8.1 and Windows 10

Integration of push notifications on Windows 8.1 and Windows 10

{% hint style="danger" %}
**This generation of SDK is deprecated and is no longer supported.**\
Information about the [current version can be found here](/integration/integration-of-sdk-v2/push-notifications/windows-uwp).
{% endhint %}

## General information

To enable Push Notifications you will have to perform the following actions:

* Add the application to your space in devtodev system
* Activate Windows Messaging Service ang get SID and Client Secret values
* Add SID and Client Secret to the application integration settings in devtodev system
* Integrate devtodev SDK to the application (see the "SDK integration" section to learn more how to integrate and initialize devtodev SDK)
* Add several lines of the code to switch on the push notification in the SDK
* Create a campaign for sending push notifications in "Push" section

## How to get SID and Client Secret

1. Go to the application settings in your Windows Store dashboard\
   ![](https://www.devtodev.com/upload/images/win_integration_0.png)
2. Open Push Notifications submenu in Services menu\
   ![](https://www.devtodev.com/upload/images/win_integration_1.png)
3. Go to Live Services site:\
   ![](https://www.devtodev.com/upload/images/win_integration_2.png)
4. "Package SID" and "Client secret" will be your SID and Client Secret strings respectively\
   ![](https://www.devtodev.com/upload/images/win_integration_3.png)

## Implementation to app

1. Integrate devtodev SDK to your project. Even if you don't need devtodev analytics in your app, you should call DevToDev.SDK.Initialize(string appKey, string appSecret).
2. Add the following source after DevToDev.SDK.Initialize(string appKey, string appSecret) is called:

   ```
   //It is called when push token is received successfully
   PushManager.PushTokenReceived = (pushToken) => {
      //pushToken - the string contains the push token
   };

   //It is called when there is an error in push token delivery.
   PushManager.PushTokenFailed = (error) => {
      //error - the error string. This function will be called when push token have not been obtained.
   };

   //It is called when push notification is received.
   PushManager.PushReceived = (PushType type, IDictionary<string, string> pushAdditionalData) => {
      //type - type of the push message
      //params - IDictionary<string, string> with the custom user parameters form the push message
   };

   //It is called when push notification is opened.
   PushManager.PushOpened = (PushMessage pushMessage, ActionButton actionButton) => {
      //pushMessage - DevToDev.PushMessage. Represents toast notification message
      //actionButton - DevToDev.ActionButton. Windows 10 only! 
      //Represents toast button that was clicked. Could be null if toast body was clicked
   };

   DevToDev.PushManager.Initialize();
   ```

   The PushType can have one of the following values:

   ```
   public enum PushType {
      ToastNotification, //Notification that can be seen by a user. 
      SilentNotification //Raw-notification. A user can't see it.
   }
   ```
3. The control of the current value of a badge. When an app is launched the current value of a badge is reset to zero by default. In order to disable automatic resetting to zero and manually control the value of a badge use the following methods:

   ```
   //Disables automatic clearing of a badge at start. 
   //Must be called before DevToDev.PushManager.Initialize();
   DevToDev.PushManager.AutoClearBadgeOnStart = false; 

   //Decreases the current value of a badge on "number" units.
   DevToDev.PushManager.DecreaseBadge(int number);

   //Clears the current value of a badge.
   DevToDev.PushManager.ClearBadgeCount();
   ```
4. **Attention! There is a difference in the implementation of the elements mentioned below for Windows 8.1+ and Windows 10+ projects.** &#x20;

   **Windows 8.1+:** Put the following source in your Application class (usually it is App.xaml.cs file) at the end of the *OnLaunched(LaunchActivatedEventArgs e)* function. For Example:

   ```
   protected override void OnLaunched(LaunchActivatedEventArgs e) {
      //...other source
      DevToDev.PushManager.HandleToastNavigation(e.Arguments);
   }
   ```

   **Windows 10+:** Put the following source in your Application class (usually it is App.xaml.cs file) at the end of the *OnLaunched(LaunchActivatedEventArgs e) and OnActivated(IActivatedEventArgs args)​* functions. For Example:

   ```
   protected override void OnLaunched(LaunchActivatedEventArgs e) {
      //...other source
      DevToDev.PushManager.HandleToastNavigation(e.Arguments);
   }

   protected override void OnActivated(IActivatedEventArgs args) {
      //...other source
      if (args.Kind == ActivationKind.ToastNotification) {
          var toastArgs = args as ToastNotificationActivatedEventArgs;
          DevToDev.PushManager.HandleToastNavigation(toastArgs.Argument);
      }
   }
   ```
5. Make sure that these functions are enabled in Package.appmanifest of you project (the flag "Toast capable" is enabled by default for Windows 10+ projects, it is absent in the manifest).\
   ![](https://www.devtodev.com/upload/images/IMG_24102016_180635.png)
6. &#x20;Add the following two Background Tasks in Package.appmanifest:\
   ![](https://www.devtodev.com/upload/images/IMG_24102016_182326.png)\
   ![](https://www.devtodev.com/upload/images/IMG_24102016_182253.png)
7. Keep in mind that your application must be built with the same Windows Store preferences you used in Chapter 3.2. In the "Create App Packages" window you have to log in with your Live ID and pick the appropriate application form the list. A file *Package.StoreAssociation.xml* will be added into the Project.\
   ![](https://www.devtodev.com/upload/images/win_integration_4.png)

## Changing the application settings in devtodev system

1\. Proceed to Settings of your app.

![](/files/-M4SYKJsLHcg3f_o-D2o)

2\. Go to PUSH NOTIFICATIONS page in Settings and insert the previously received Package SID and Client secret to appropriate fields in Push notifications section.

3\. If the Package SID and Client secret are correct, you will see the following result:

![](https://www.devtodev.com/upload/images/3%283%29.png)

## Creating a new push notification in devtodev interface

1. Open PUSH NOTIFICATION section and click on "Add new campaign" button
2. Fill in campaign name, select an app for delivery\*
3. Choose the user group to send a message. You can choose existing segment or create a new one
4. Enter toast or tile details
5. Schedule the delivery
6. That's it!

{% hint style="warning" %}
**\*Attention!** You can create a campaign only after at least one push token comes from devtodev SDK integrated to your application. Otherwise the app will not be displayed in the list.
{% endhint %}


# Unity

{% hint style="danger" %}
**This generation of SDK is deprecated and is no longer supported.**\
Information about the [current version can be found here](/integration/integration-of-sdk-v2/push-notifications/unity).
{% endhint %}

{% hint style="info" %}
**Push Notifications are available only for the supported platforms: iOS, Android, Windows Store/Windows Phone 8.1/10.**
{% endhint %}

To enable Push Notifications you will have to perform the following actions:

* Add the application to your space in devtodev system
* **Android.** Get API key from Google APIs Console. It is necessary to activate Google Cloud Messaging for Android before key generation. Detailed information on how to receive an API key you can find in native [Android devtodev SDK documentation](/integration/integration-of-sdk/analytics-integration/android)
* **iOS.** Generate Developer or Production Certificate for the application and get Private key file (.p12) on its basis. Detailed information on how to receive a Private key file you can find in native [iOS devtodev SDK documentation](/integration/integration-of-sdk/analytics-integration/ios)
* Submit the data to the application settings in devtodev system
* Integrate devtodev SDK to the application (see the "[SDK integration](/integration/integration-of-sdk/analytics-integration/unity)" to learn more about integrating and initializing devtodev SDK)
* Add several lines of the code to switch in the push notification to the SDK
* Create a campaign for sending push-notifications in "Push" section

## SDK Integration

### **Android platform features:**

Go to [Firebase console](https://console.firebase.google.com/) and then to your project or create a new one. [Here is](/integration/integration-of-sdk/push-notifications/android) complete guide on adding your project to Firebase console and enabling Cloud messaging.

Download **google-services.json** from your Firebase console. Add this file into your project’s Assets folder.

Please do the following to find google-services.json:

1. Choose your project in the Firebase console
2. Choose project settings in the Project overview

![](/files/-MOgFzDhYftTYX2kFHjb)

3\. Scroll down to the SDK setup and configuration. Click on the google-services.json

![](/files/-MOgGENi2haTgMRQ-Iw5)

#### Using devtodev and Firebase Messaging services at the same time

If you want to use both devtodev and Firebase Messaging services at the same time, you need to disable Firebase listener.

* Find androidmanifest.xml used in your app. If you don’t use Custom Manifest, you need to create it. Tick the Custom Main Manifest checkbox:

  ![](/files/-M_0CJIty0Ui1ge-fcsJ)

  \
  You can read more about the manifest [here](https://docs.unity3d.com/Manual/android-manifest.html)**.**
* Add the following line to the “Application” section:

```markup
<service android: name = "com.google.firebase.messaging.cpp.ListenerService"
android: exported = "true"
android: enabled = "false"
tools: node = "replace" />
```

* You should get something like this:

```markup
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.devtodev.unitysdk2" android:versionCode="1" android:versionName="1.0">
  <application android:label="@string/app_name" android:icon="@drawable/app_icon">
    <service android:name="com.google.firebase.messaging.cpp.ListenerService" android:exported="true" android:enabled="false" tools:node="replace" />
    <!-- The MessagingUnityPlayerActivity is a class that extends
         UnityPlayerActivity to work around a known issue when receiving
         notification data payloads in the background. -->
    <activity android:name="com.google.firebase.MessagingUnityPlayerActivity" android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
      <meta-data android:name="unityplayer.UnityActivity" android:value="true" />
    </activity>
    <service android:name="com.google.firebase.messaging.MessageForwardingService" android:exported="true" />
  </application>
</manifest>
```

### **Windows Store 10 platform features*****:***

Build a Windows Store App in Unity. After the app is built, Visual Studio project will be created. Proceed with the following changes.

{% hint style="info" %}
There is a difference in the implementation of the elements mentioned below for **different types of projects**:
{% endhint %}

**.NET + D3D:** Put the following source in your App class (usually it is an App.cs file) at the end of the ApplicationView\_Activated(CoreApplicationView sender, IActivatedEventArgs args)​ function.

```
private void ApplicationView_Activated(CoreApplicationView sender, IActivatedEventArgs args) {
    //...other code
    DevToDev.ActivatedEventHandler.Handle(args);
}
```

**.NET + XAML:** Put the following source in your App class (usually it is an App.xaml.cs file) at the end of the OnLaunched(LaunchActivatedEventArgs args) and OnActivated(IActivatedEventArgs args) functions.

```
protected override void OnLaunched(LaunchActivatedEventArgs e) {
    //...other code
    DevToDev.ActivatedEventHandler.Handle(e);
}

protected override void OnActivated(IActivatedEventArgs args) {
    //...other source
    DevToDev.ActivatedEventHandler.Handle(args);
}
```

**IL2CPP + XAML:** Put the following source in your App class (usually it is App.xaml.cpp file).\
Add several lines of code in a generated App.xaml.cpp class.\
After defining headers:

```
//...headers
extern "C" __declspec(dllimport) void __stdcall AddActivatedEventArgs(IInspectable* activatedEventArgs);
```

And at the end of of the App::OnLaunched(LaunchActivatedEventArgs^ e) and App::OnActivated(IActivatedEventArgs^ args) functions.

For Example:

```
void App::OnActivated(IActivatedEventArgs^ args) {
    //...other code
    AddActivatedEventArgs(reinterpret_cast<IInspectable*>(static_cast<Platform::Object^>(args)));
}

void App::OnLaunched(LaunchActivatedEventArgs^ e) {
    //...other code
    auto args = static_cast<IActivatedEventArgs^>(e);
	AddActivatedEventArgs(reinterpret_cast<IInspectable*>(static_cast<Platform::Object^>(args)));
}
```

**IL2CPP + D3D:** Put the following source in your App class (usually it is App.cpp file).\
Add several lines of code in a generated App.cpp class.\
After defining headers:

```
//...headers
extern "C" __declspec(dllimport) void __stdcall AddActivatedEventArgs(IInspectable* activatedEventArgs);
```

And at the end of of the App::OnActivated(CoreApplicationView^ sender, IActivatedEventArgs^ args) function.

```
void App::OnActivated(CoreApplicationView^ sender, IActivatedEventArgs^ args) {
    //...other code
    AddActivatedEventArgs(reinterpret_cast<IInspectable*>(static_cast<Platform::Object^>(args)));
}
```

Make sure that these functions are enabled in Package.appmanifest of you project (the flag "Toast capable" is enabled by default for Windows 10+ projects, it is absent in the manifest).

![](https://www.devtodev.com/upload/images/IMG_24102016_180635.png)

Add the following three Background Tasks in Package.appmanifest:

Entry point for Push Notification tasks type:

```
devtodev.background.PushNotificationTriggerTask
```

Entry points for System Event tasks type:

```
devtodev.background.ToastNotificationActionTriggerTask
devtodev.background.ToastNotificationHistoryChangedTriggerTask
```

You must also associate your application with the Windows Store app (otherwise push notifications will not be delivered). Open "Store->Associate App with the Store" menu, login with your Live ID and pick the appropriate application form the list. A file *Package.StoreAssociation.xml* will be added into the Project.\
![](https://www.devtodev.com/upload/images/win_integration_4.png)

### **The specificity of integration on iOS platform**

1. Build an iOS App in Unity. After the app is built, Xcode project will be created. Proceed with the following changes
2. Enable push notifications in your Xcode project
3. The library provides support for iOS 10 notification attachments, such as images, animated gifs, and video. In order to take advantage of this functionality, you will need to create a notification service extension alongside your main application
4. Create a new iOS target in Xcode (File -> New -> Target) and select the Notification Service Extension type
5. ​In Member Center, the Push Notifications service will appear as Configurable (not Enabled) until you create a client SSL certificate

![](https://www.devtodev.com/upload/images/3%2814%29.png)

Add devtodevAppExtensions.framework to newly created extension. Make sure that Deployment Target is pointed as iOS 10.0 or higher:

![](https://www.devtodev.com/upload/images/Unity-iPhone.xcodeproj%202016-12-02%2016-22-07.jpg)

Make sure that field Architectures contains "Standard architectures armv7, arm64" setting both in the project and the extension build settings:

![](https://www.devtodev.com/upload/images/Unity-iPhone.xcodeproj%202016-12-02%2016-15-56.jpg)

Modify your extension:

1. Delete all dummy source code for your new extension
2. Inherit from DTDMediaAttachmentExtension in NotificationService

```
//NotificationService.h
#import <devtodevAppExtensions/devtodevAppExtensions.h>

@interface NotificationService : DTDMediaAttachmentExtension

@end

//NotificationService.m
#import "NotificationService.h"

@interface NotificationService ()

@end
```

### **Adding Capabilities**

Use Xcode to enable push notifications in the target’s Capabilities pane:

![](https://www.devtodev.com/upload/images/8%285%29.png)

Enable Background Modes and Remote notifications under the target’s Capabilities section:

![](https://www.devtodev.com/upload/images/7%287%29.png)

#### **Two ways to integrate push notifications:**

* **Using the graphic interface:**
  1. Open the Window/devtodev menu element
  2. Switch Push Notifications tumbler on
  3. If you need to use push token for some aims or to handle the getting of notifications by user, add the GameObject with the following function to the scene:

     ```
     public void PushReceived(IDictionary<string, string> pushAdditionalData) {
          //pushAdditionalData - push-notification data that you send to your app
     }

     public void PushOpened(DevToDev.PushMessage pushMessage, DevToDev.ActionButton actionButton) {
          //pushMessage - DevToDev.PushMessage. Represents toast notification message
          //actionButton - DevToDev.ActionButton. Represents toast button that was clicked. Could be null if toast body was clicked
     }

     public void PushTokenFailed(string error) {
          //handle push-notifications error here
     }

     public void PushTokenReceived(string pushToken) {
          //pushToken - your push token
     }
     ```
  4. Set the target game object, needed script and functions in the interface:\
     [![](https://www.devtodev.com/upload/images/Unity%202018.1.0b9%20Personal%20%2864bit%29%20-%20ChoiseScene.unity%20-%20application%20-%20PC%2C%20Mac%20%26%20Linux%20Standalone%20%28Personal%29%20%3COpenGL%204.1%3E%202018-06-07%2017-37-06.png)](https://monosnap.com/file/l3IGh9vZBpE0PZzspUrUIbS4Eo2z40)
* **Using code:**\
  **Before** calling Analytics.Initialize add the following strings:

```
public void PushReceived(IDictionary<string, string> pushAdditionalData) {
     //pushAdditionalData - push-notification data that you send to your app
}

public void PushOpened(DevToDev.PushMessage pushMessage, DevToDev.ActionButton actionButton) {
     //pushMessage - DevToDev.PushMessage. Represents toast notification message
     //actionButton - DevToDev.ActionButton. Represents toast button that was clicked.
     //               Could be null if toast body was clicked
}

public void PushTokenFailed(string error) {
     //handle push-notifications error here
}

public void PushTokenReceived(string pushToken) {
     //pushToken - your push token
}

DevToDev.PushManager.PushReceived = PushReceived;
DevToDev.PushManager.PushOpened = PushOpened;
DevToDev.PushManager.PushTokenFailed = PushTokenFailed;
DevToDev.PushManager.PushTokenReceived = PushTokenReceived;

DevToDev.PushManager.PushNotificationsOptions = (DTDNotificationOptions.Alert | DTDNotificationOptions.Badge | DTDNotificationOptions.Sound | DTDNotificationOptions.Provisional); //Notification options for iOS, optional property
DevToDev.PushManager.PushNotificationsEnabled = true; 

// FOR ANDROID ONLY! Optional. Using custom push-notification icons on Android.
// <summary> To set the custom icons to be shown in push-notification on the SDK part,
// use the following methods.
// Attention! Icons which set in the push-notification wizard 
// have priority over the icons which set in these methods.</summary>
// <param name="iconName">Icon file from resources of your app
// (from Assets/Plugins/Android/res folder)</param>

//FOR ANDROID ONLY! To set the small icon:
DevToDev.PushManager.CustomSmallIcon = iconName;

//FOR ANDROID ONLY! To set the large icon: 
DevToDev.PushManager.CustomLargeIcon = iconName;
```

## Creating a new push-notification in devtodev interface

&#x20;1\. Open PUSH NOTIFICATIONS section and click on the "Add new campaign" button

![](/files/-M4T-DVK_l5H9o-YSQPh)

2\. Fill in the campaign name, select an app for delivery\
3\. Choose a user group to send a message. You can choose an existing segment or create a new one\
4\. Enter notification details\
5\. Schedule the delivery\
6\. That's it!

{% hint style="warning" %}
You can create a campaign only after at least one push token comes from devtodev SDK integrated into your application. Otherwise, the app will not be displayed in the list.
{% endhint %}


# Abode Air

Integration of push notifications on Android and iOS using devdodev SDK for Adobe Air

{% hint style="danger" %}
**This generation of SDK is deprecated and is no longer supported.**
{% endhint %}

{% hint style="warning" %}
Push Notifications are availible only for iOS and Android.
{% endhint %}

## Android push notifications

### Obtaining configuration file for firebase push notifications

Go to [Firebase console](https://console.firebase.google.com/) and then to your project or create a new one.

&#x20;   Current project:&#x20;

![](https://www.devtodev.com/upload/images/6-1.png)

![](https://www.devtodev.com/upload/images/7-1.png)

&#x20;   New project:

![](https://www.devtodev.com/upload/images/1%2815%29.png)

![](https://www.devtodev.com/upload/images/2%2811%29.png)

![](https://www.devtodev.com/upload/images/3%2813%29.png)

![](https://www.devtodev.com/upload/images/4%287%29.png)

Save the google-services.json file and add it to your app so that it is placed inside the Assets folder in a ready .apk file. If you use Flash Builder IDE simply copy it in the root folder, Flash Builder will do the rest for you.

### Implementation to app

1. Update Adobe Air SDK. **Pay attention that version of the Adobe Air SDK should be at least 22.0, otherwise FCM classes will not be imported into your project.** However, you still can use Analytics, but application will not be able to accept Push Notifications.
2. Add dependencies to your project. These libraries are located in the same archive with com.devtodev.SDK.ane extension in the dependencies folder. These extensions contain firebase, gps и android-support libraries Java, that are used for sending Push Notifications. If you have already imported firebase-auth, firebase-common, firebase-iid, firebase-messaging, play-services-auth, play-services-base, play-services-basement, support-v4 or they are included in other extensions, you don't need to import them again. In any case, we recommend to use data from the library of a version not lower than 25. \
   \
   Add the following to the manifest file of your app:

   ```
   <application>
       <extensions>
           <extensionID>com.devtodev.air.extensions.gps-auth</extensionID>
           <extensionID>com.devtodev.air.extensions.gps-base</extensionID>
           <extensionID>com.devtodev.air.extensions.gps-basement</extensionID>
           <extensionID>com.devtodev.air.extensions.supportv4</extensionID>
           <extensionID>com.devtodev.air.extensions.firebase-auth</extensionID>
           <extensionID>com.devtodev.air.extensions.firebase-common</extensionID>
           <extensionID>com.devtodev.air.extensions.firebase-iid</extensionID>
           <extensionID>com.devtodev.air.extensions.firebase-messaging</extensionID>
           <!-- this library should be already added for analytics -->
           <extensionID>com.devtodev.SDK</extensionID>
           ...
       </extensions>
   </application>
   ```
3. Add the following to your application's manifest:

   ```
   <!--Replace 'com.example.application' to your package -->
   <manifest package="com.example.application" ...>
       <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
       <permission android:name="com.example.gcm.permission.C2D_MESSAGE" android:protectionLevel="signature" />
       <uses-permission android:name="com.example.gcm.permission.C2D_MESSAGE" />
   	<application>
   		<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />
                       
           <receiver android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver" android:exported="true" android:permission="com.google.android.c2dm.permission.SEND" >
               <intent-filter>
                   <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                   <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
                   <category android:name="com.example.application" />
               </intent-filter>
           </receiver>
                       
           <service android:name="com.devtodev.push.logic.DTDFcmMessagingService" android:exported="false">
               <intent-filter>
                   <action android:name="com.google.firebase.MESSAGING_EVENT"/>
               </intent-filter>
           </service>
                       
           <service android:name="com.devtodev.push.logic.DTDFcmInstanceIdService" android:exported="false">
               <intent-filter>
                   <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
               </intent-filter>
           </service>
                       
   		<receiver android:name="com.devtodev.push.logic.PushClickReceiver" android:enabled="true" android:exported="true">
   		    <intent-filter>
   			    <action android:name="com.devtodev.android.push.CLICKED" />
   			</intent-filter>
   		</receiver>
   					
   		<activity android:name="com.devtodev.push.logic.PushHandlerActivity"/>
   	</application>
   </manifest>
   ```
4. Add the following imports to your source:

   ```
   import com.devtodev.sdk.push.DevToDevPushManager;
   import com.devtodev.sdk.push.logic.ActionButton;
   import com.devtodev.sdk.push.logic.PushMessage;
   ```
5. Add the push notifications initialization **before** the DevToDev.init(appKey:String, appSecret:String) method was called:

   ```
   DevToDevPushManager.setOnFailedToRegisteredForPushNotifications(onPushTokenFailed);
   DevToDevPushManager.setOnRegisteredForPushNotifications(onPushToken);
   DevToDevPushManager.setOnPushNotificationsReceived(onPushReceived);					
   DevToDevPushManager.setOnPushNotificationOpened(onPushOpened);					
   DevToDevPushManager.setPushNotificationsEnabled(true);
   ```

   *onPushToken*, *onPushTokenFailed,* *onPushReceived and onPushOpened* are the functions that take following arguments:

   ```
   /**
   * @param token - push token
   */
   protected function onPushToken(param:String):void {
   }
   			
   /**
   * @param error - error message  
   */
   protected function onPushTokenFailed(param:String):void {
   }
   			
   /**
   * @param pushData - Dictionary with push message and custom push fields
   */
   protected function onPushReceived(pushData:Dictionary):void {
   }
   			
   /**
   * @param message - PushMessage. Represents toast notification message
   * @param button - ActionButton. Represents toast notification button that was clicked. Could be null if notification body was clicked
   */
   protected function onPushOpened(message:PushMessage, button:ActionButton):void {
   }
   ```

### Changing the application settings in devtodev system

1\. Proceed to Setting -> PUSH NOTIFICATIONS:

![](/files/-M4T29ZE-QTdM-QjhIgW)

2\. Insert the previously received Server API Key to the API Key field in Push notifications section.

![](/files/-M4T2YiMxMdWHNDHxT5V)

3\. If the Server API Key is correct, you will see the following result

![](/files/-M4T2i9T4lU7BfKtGp7p)

### Creating a new push-notification in devtodev interface

1. Open PUSH NOTIFICATIONS section and click on 'Add new campaign' button
2. Fill in campaign name, select an app for delivery
3. Choose user group to send a message. You can choose existing segment or create a new one.
4. Enter notification details
5. Make some tests and correct the message if it's required
6. Schedule the delivery
7. That's it!

{% hint style="warning" %}
You can create a campaign only after at least one push token comes from devtodev SDK integrated to your application. Otherwise the app will not be displayed in the list.
{% endhint %}

## iOS push-notifications

To enable Push Notifications, please perform the following actions:

1. Add the application to your space in devtodev system
2. Generate Developer or Production Certificate for the application and get Private key file (.p12) on its basis
3. Submit the data to the application settings in devtodev system
4. Integrate devtodev SDK to the application (see the “SDK integration” division to learn more about integrating and initializing devtodev SDK)
5. Add several lines of the code to switch in the push notification to the SDK
6. Create a campaign for sending push notifications in “Push” section

### Certificate generation

1. Open "Keychain access" utility (Launchpad → Other) and choose "Request a Certificate From a Certificate Authority" option.\
   ![21.jpg](https://lh5.googleusercontent.com/3xpSEXC_OmK8OcRORohP2SGEHXm9Ya6oLusx8k7r78bhhQdErp4b2za10eh6Tmy_UQSXt6pqGFr0LxTuw0i5RkjLSlDkq1734RTv4TVhUykvbHPv3wtC1Ro9VJVwCeY1Zk6QUhw)
2. Fill in all requied fields in Certificate Assistant window, set flag an "Saved on disk" item and click "Continue". Save the file.\
   ![22.jpg](https://lh3.googleusercontent.com/cbgKp4Nxu5t6lf1u1do2LkXyIGyS8lRhPCOaNHGXO3CNZbU41QIfqox9K7opMfirV0dqpKaZMQQ17NzKC708VnPbNKpdBTlq2tHutkKYkkj8oY4qyjk24uCG86nfAsp479aRW1U)
3. Log in at iOS Provisioning Portal. Open "App IDs" section, choose your app and click "Edit".\
   ![23.jpg](https://lh6.googleusercontent.com/_d2oWTednaPdxzQdbJ-xuJnQm7Bc_AxBHNEPcnARgW9utJZYptDknQ6liuWOpmIliQTYXYVuNCPXK6Aunc-JUPTs75zLQqapGswNGH3EP2HQ_GYSJvk3XYi7GJ4gSNujpBTKicA)
4. Activate "Push Notifications" option and click on "Create Certificate"\
   ![24.jpg](https://lh3.googleusercontent.com/YD22FQem-WYeRcOkLPvhfcOsYS0y1oQ6Z9FgZ-SUm1E4olFg7oIkA73lBGheiJkngmL17UQ354TLg8C0WfyzXvswzi5Soih-aFzW3n4pCDcTmJBG5-AnTwzDSMrh5gsmmzJQoPo)
5. At first, you need to generate Certificate Signing Request. We have already done it, so you can just click "Continue".
6. At next step you need to upload the CSR to the Apple server. Choose the CSR-file and click "Generate".
7. Certificate generation takes just a few seconds. After generation is finished click "Download" and then "Done".\
   ![25.jpg](https://lh3.googleusercontent.com/Gyllcwk6WK5QxkhthLAnp0CW7iJBZJwcNjPMiAszk0D8WtwggxSvOEpcZ9oRUA7AQETKV2UY6VnDryZBFQ4Los2ehrzjqXSomBP09ciLCR9yofXFGhR2KFyRDxOxCfhYU2_doHA)
8. You'll need to repeat this process to generate the Production Certificate when your app will be ready for release. All steps are the same.

#### **Convert iPhone developer certificate into a P12 file on Mac OS**

Follow these steps to export the certificate from Apple web-site to the P12-file:

1. Open "Keychain access" application
2. If the certificate hasn't been added to keychain access yet, choose "File" →  "Import". Find the certificate file (CER-file) provided by Apple
3. Choose "Keys" section in "Keychain access" application
4. Choose personal key associated with your iPhone developer certificate. Personal key is identified by open certificate associated with it "iPhone developer: ". Choose "File" → Export objects. Save key as .p12
5. You'll be suggested to create a password which is used when you need to import the key to another computer.

#### **Convert iPhone developer certificate into a P12 file on Windows OS**

Convert Apple certificate file to the PEM-file. Start the following command-line operation from bin catalog OpenSSL.

```
openssl x509 -in developer_identity.cer -inform DER -out developer_identity.pem -outform PEM
```

Convert personal key from Mac OS keychain to the PEM-key:

```
openssl pkcs12 -nocerts -in mykey.p12 -out mykey.pem
```

Now you are able to create P12-file using PEM-key and iPhone developer certificate:

```
openssl pkcs12 -export -inkey mykey.key -in developer_identity.pem -out iphone_dev.p12
```

If you are using key from Mac OS keychain than choose PEM-version created at previous step.

Otherwise you can use OpenSSL key for Windows OS.

### Upload the certificate to the site

Upload the .p12-file into Integration section of application settings panel  (Settings -> PUSH NOTIFICATIONS):

![](/files/-M4OUZotk6PsJCCGiFhB)

After the certificates has been generated you can start to integrate Push SDK into you app.

### SDK Integration

1. Add the following to your application's manifest. **Don't forget that the minimum supported version is iOS 7**:&#x20;

   ```
   <iPhone>
         <InfoAdditions><![CDATA[
            <key>MinimumOSVersion</key>
            <string>7.0</string>
   		 <key>UIDeviceFamily</key>
   		 <array>
   		    <string>1</string>
   			<string>2</string>
   		 </array>
            ...
   	 ]]></InfoAdditions>

        <Entitlements>
    	    <![CDATA[
           	<key>aps-environment</key>
            	<string>development</string>
           ]]>
        </Entitlements>  
   </iPhone>
   ```

   **Attention! If you use Production Certificate for signing application package, don't forget to change "development" value to "production".**
2. Add the following imports to your source:

   ```
   import com.devtodev.sdk.push.DevToDevPushManager;
   import com.devtodev.sdk.push.logic.ActionButton;
   import com.devtodev.sdk.push.logic.PushMessage;
   ```
3. Add the push notifications initialization **before** the DevToDev.init(appKey:String, appSecret:String) method was called:

   ```
   DevToDevPushManager.setOnFailedToRegisteredForPushNotifications(onPushTokenFailed);
   DevToDevPushManager.setOnRegisteredForPushNotifications(onPushToken);
   DevToDevPushManager.setOnPushNotificationsReceived(onPushReceived);					
   DevToDevPushManager.setOnPushNotificationOpened(onPushOpened);					
   DevToDevPushManager.setPushNotificationsEnabled(true);
   ```

   *onPushToken*, *onPushTokenFailed,* *onPushReceived and onPushOpened* are the functions that take following arguments:

   ```
   /**
   * @param token - push token
   */
   protected function onPushToken(param:String):void {
   }
   			
   /**
   * @param error - error message  
   */
   protected function onPushTokenFailed(param:String):void {
   }
   			
   /**
   * @param pushData - Dictionary with push message and custom push fields
   */
   protected function onPushReceived(pushData:Dictionary):void {
   }
   			
   /**
   * @param message - PushMessage. Represents toast notification message
   * @param button - ActionButton. Represents toast notification button that was clicked. Could be null if notification body was clicked
   */
   protected function onPushOpened(message:PushMessage, button:ActionButton):void {
   }
   ```
4. Compile and run the app. You will need a device, because simulator does not support push notifications.

Xcode will automatically choose new provisioning profile. If an error occurred during the launch make sure that there is a correct profile set in the Code Signing Identity. You'll be asked to confirm push notifications. An app will request permission only once, if user confirm it - notifications will be accepted otherwise he wont get any push messages from your app. User can change it in device settings.

### Creating a new push notification in devtodev interface

1\. Open PUSH NOTIFICATIONS section and click on "Add new campaign" button.

![](/files/-M4OV2lLOu1F103aN-ws)

2\. Fill in campaign name

{% hint style="info" %}
You can create a campaign only after at least one push token comes from devtodev SDK integrated to your application. Otherwise the app will not be displayed in the list.
{% endhint %}

![](/files/-M4OXSl6_bvIp7uzCqjY)

3\. Choose user group to send a message. You can choose existing segment or create a new one.

![](/files/-M4OYMgORidREO2WmZBb)

4\. Enter notification details

![](/files/-M4OYbs-hLaOWBnRZcOz)

5\. Test push notification (or skip this step)

![](/files/-M4OYxPUnYWwj4P5AABg)

6\. Confirm push gateway

![](/files/-M4OZNNwp3hvBFffXDrY)

7\. Schedule the delivery

![](/files/-M4OZP2pshb4ZIoODEbz)

8\. That's it!


# UE4

Integration of push notification on UE4

{% hint style="danger" %}
**This generation of SDK is deprecated and is no longer supported.**\
Information about the [current version can be found here](/integration/integration-of-sdk-v2/push-notifications/unreal-engine).
{% endhint %}

## General information

### **To enable Push Notifications you will have to perform the following actions:**

* Add the application to your space in devtodev system
* **Android.** Get API key from Google APIs Console. It is nessesary to activate Google Cloud Messaging for Android before key generation. Detailed information on how to receive an API key you can find in native Android devtodev SDK documentation
* **iOS.** Generate Developer or Production Certificate for the application and get Private key file (.p12) on its basis. Detailed information on how to receive a Private key file you can find in native iOS devtodev SDK documentation
* Submit the data to the application settings in devtodev system
* Integrate devtodev SDK to the application (see the "SDK integration" division to learn more about integrating and initializing devtodev SDK)
* Set Push Notification Enabled in blueprint.
* Create a campaign for sending push notifications in "Push" section

### Project settings

Set Push Notification Enabled in blueprint.

![](https://www.devtodev.com/upload/images/image%281%29.png)

## Creating a new push notification in devtodev interface

1. Open PUSH NOTIFICATIONS section and click on "Add new campaign" button
2. Fill in campaign name, select an app for delivery\*
3. Choose user group to send a message. You can choose existing segment or create a new one
4. Enter notification details
5. Schedule the delivery
6. That's it!

{% hint style="warning" %}
You can create a campaign only after at least one push token comes from devtodev SDK integrated to your application. Otherwise the app will not be displayed in the list.
{% endhint %}


# 3rd Party Sources

{% content-ref url="/pages/-Lyh8n51DHDiKYZ-yNXh" %}
[Attribution Trackers](/3rd-party-sources/3-rd-party-attribution)
{% endcontent-ref %}

{% content-ref url="/pages/-Lyh9gtiAwm3G7A07qYP" %}
[App Marketplace Data](/3rd-party-sources/app-marketplace-data)
{% endcontent-ref %}

{% content-ref url="/pages/-M7CeZC\_JPlxzSeT81dE" %}
[Ad revenue](/3rd-party-sources/ad-revenue)
{% endcontent-ref %}

{% content-ref url="/pages/CXvScIEwXY3d7o0voDtt" %}
[Cohort export](/3rd-party-sources/cohort-export)
{% endcontent-ref %}


# Attribution Trackers

3rd party attribution sources integration

devtodev has integration with the following attribution trackers:

* [AppsFlyer](/3rd-party-sources/3-rd-party-attribution/appsflyer)
* [Adjust](/3rd-party-sources/3-rd-party-attribution/adjust)
* [Branch.io](/3rd-party-sources/3-rd-party-attribution/branch.io)
* [Kochava](/3rd-party-sources/3-rd-party-attribution/kochava)
* [Tenjin](/3rd-party-sources/3-rd-party-attribution/tenjin)
* [Tune (MAT)](/3rd-party-sources/3-rd-party-attribution/tune-mat)
* [Singular](/3rd-party-sources/3-rd-party-attribution/singular)

If the tracker you are using is not in the list, you can send data via [Custom postback API.](/3rd-party-sources/3-rd-party-attribution/custom-postback-api)


# AppsFlyer

## Integration

1. Copy ***devtodev API key*** from “3rd Party Attribution. AppsFlyer” panel on " Settings -> 3rd party sources -> Attribution tracking" page **in devtodev system**. **Don't forget to turn on the service.**&#x20;

<figure><img src="/files/lxgA6cp1VXDjRepmv91s" alt=""><figcaption></figcaption></figure>

2. Sign in with [AppsFlyer](https://appsflyer.com).
3. Open the [devtodev Tech Partner integration page](https://hq1.appsflyer.com/partner-marketplace/partner/devtodevrai_int-tech_partner) and start manage integration.

<figure><img src="/files/BxwZpg3sL7Hv40e2PP0e" alt=""><figcaption></figcaption></figure>

4. Select your app, paste the devtodev API key you copied before, select "All media sources, including organic", activate partner and click "Save integration". Integration completed.

<figure><img src="/files/WBiSB7cptWmiqnmRsDdQ" alt=""><figcaption></figcaption></figure>

For iOS applications we recommend turning off the Advanced Privacy (for iOS 14.5+ and later) option.

<figure><img src="/files/XKPWyRN4wJsHGyyQwmF5" alt=""><figcaption></figcaption></figure>

### Improve matching

{% hint style="info" %}
To improve the synchronization of the user data obtained by devtodev with the install data from AppsFlyer, we recommend that you follow the guideline below. The following recommendation is especially important for iOS since Advertising ID (IDFA) is no longer always available, and Vendor ID (IDFV) is not always sent by AppsFlyer. The best solution here is to send AppsFlyer’s user ID data to devtodev.

You need to make some changes to the devtodev SDK integration. You need to send a string parameter named "**`ad_tracker_id`**" to the [user profile custom property](/integration/integration-of-sdk-v2/setting-up-events/user-profile#custom-user-property) while assigning to its value an AppsFlyer ID, which can be [obtained from AppsFlyer SDK](https://support.appsflyer.com/hc/en-us/articles/207032066-iOS-SDK-V6-X-integration-guide-for-developers#additional-apis-get-appsflyer-id).

If you have multiple mobile apps for which you would like to set up this integration, you will need to follow these steps for each one of your mobile apps individually. \
\
You can find this field later as `MMP_ID` in the [User card](/reports-and-functionality/project-related-reports-and-fuctionality/users#users) or `mmpid` in the [users table](/reports-and-functionality/space-related-reports-and-functionality/sql#users-table-specific-fields) in SQL.
{% endhint %}

## CPI data integration

devtodev allows our customers to query CPI data from AppsFlyer via Pull API.&#x20;

{% hint style="info" %}
AppsFlyer has API daily rate limits, so adding CPI integration results in a single daily request per app.
{% endhint %}

Integration can be enabled by checking the appropriate box.\
You will need an [API token V.2.0](https://support.appsflyer.com/hc/en-us/articles/360004562377), which can be found on the Profile -> Security Center -> [API tokens page](https://hq1.appsflyer.com/account/api-tokens).\
For Apple platform App ID is Application ID without the 'id' prefix.\
For Android platform App ID is Android Package name.\
Note: App IDs are case sensitive.<br>

<figure><img src="/files/2JVUKnfPWTHaZZshA0YG" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/GdUeuCyMqcT3KM5tggGy" alt=""><figcaption></figcaption></figure>




---

[Next Page](/llms-full.txt/1)

