Magento 2 GDPR v3.x Developer Guide and API Reference

Downloading, Deleting and Anonymizing Customer Data

Plumrocket GDPR Magento 2 extension allows you to manage any third-party data stored in your Magento database. Use this manual to make your third-party extension GDPR compliant. Contact vendors of the third-party extensions you are using, and ask them if their extensions record any personal customer data in your Magento database. If they collect any personal data, they may become GDPR compliant by integrating their plugins with Plumrocket GDPR Extension using the provided API.

Export, delete or anonymize third-party data in 2 simple steps:

  1. Create or edit Dependency Injection (di.xml) file for your third-party plugin.
  2. Create GDPR model in the third-party plugin to manage your customer data.

Both steps are described with examples below.

Step 1. Edit Dependency Injection (di.xml) file

To export or anonymize data, you must first create or edit Dependency Injection XML file (di.xml): Open the file:

/app/code/VendorName/ModuleName/etc/di.xml

and add the code as shown below (download the template here.):

<type name="PlumrocketGDPRModelAccountProcessor">
    <arguments>
        <argument name="processors" xsi:type="array">
            <item name="VendorName_ModuleName"
                xsi:type="object">VendorNameModuleNameModelGDPRProcessorName</item>
        </argument>
    </arguments>
</type>
<type name="VendorNameModuleNameModelGDPRProcessorName">
    <arguments>
        <argument name="dataExport" xsi:type="array">
            <item name="db_field_name1" xsi:type="string" translatable="true">
                Field Alias 1</item>
            <item name="db_field_name2" xsi:type="string" translatable="true">
                Field Alias 2</item>
        </argument>

        <argument name="dataAnonymize" xsi:type="array">
            <item name="db_field_name3" xsi:type="null"/>
            <item name="db_field_name4" xsi:type="string">anonymousString</item>
            <item name="db_field_name5" xsi:type="string">anonymousEmail</item>
            <item name="db_field_name6" xsi:type="number">0</item>
       </argument>
    </arguments>
</type>

Where:

“dataExport” argument describes the data that should be exported:

  • Field “db_field_name1” will be named in CSV file as “Field Alias 1”
  • Field “db_field_name2” will be named in CSV file as “Field Alias 2”

“dataAnonymize” argument describes the data that should be anonymized:

  • Field “db_field_name3” will have value “null” after the anonymization process
  • Field “db_field_name4” will be replaced by the value of “anonymousString”. This constant is generated automatically using the following format: [Anonymization Key] + “-” + [Customer Id]
  • Field “db_field_name5” will be replaced by the value of “anonymousEmail”. This constant is generated automatically using the following format: [Anonymization Key] + “-” + “@example.com”;
  • Field “db_field_name6” will have value “0”

Please note: the “Anonymization Key” can be set in Magento System Configuration -> GDPR -> Account Removal Settings -> Anonymization Key

In this first step we can only specify which data to export or anonymize. The process of deleting data will be described in the next step.

Step 2. Create GDPR Model

In the second step we must create GDPR model file that should process all data (export / anonymize / delete) listed in the first step.

Create file:

VendorNameModuleNameModelGDPRProcessorName.php

and add the code as shown below (or download the template here.):

<?php

namespace VendorNameModuleNameModelGDPR;

use PlumrocketGDPRApiDataProcessorInterface;
use PlumrocketGDPRHelperCustomerData;
use MagentoCustomerApiDataCustomerInterface;
use VendorNameModuleNameModelResourceModelDataModelNameCollectionFactory;

/**
 * Class ProcessorName
 * @package VendorNameModuleNameModelGDPR
 */
class ProcessorName implements DataProcessorInterface
{
    /**
     * @var CustomerData
     */
    protected $customerData;
    /**
     * @var CollectionFactory
     */
    private $collectionFactory;
    /**
     * ProcessorName constructor.
     *
     * @param CustomerData $customerData
     * @param CollectionFactory $collectionFactory
     * @param array $dataExport
     * @param array $dataAnonymize
     */
    public function __construct(
        CustomerData $customerData,
        CollectionFactory $collectionFactory,
        array $dataExport = [],
        array $dataAnonymize = []
    ) {
        $this->customerData = $customerData;
        $this->collectionFactory = $collectionFactory;
        $this->dataExport = $dataExport;
        $this->dataAnonymize = $dataAnonymize;
    }

    /**
     * This method is used to export customer data based on arguments defined in di.xml file
     *
     * Expected return structure:
     * array(
     * array('HEADER1', 'HEADER2', 'HEADER3', ...),
     * array('VALUE1', 'VALUE2', 'VALUE3', ...),
     * ...
     * )
     *
     * @param CustomerInterface $customer
     *
     * @return array
     */
    public function export(CustomerInterface $customer)
    {
        $customerId = $customer->getId();
        // Retrieve your customer data from database
        $collection = $this->collectionFactory->create()->addFieldToFilter('customer_id', $customerId);
        $returnData = [];
        $i = 0;

        if (!$collection->getSize()) {
            return null;
        }

        foreach ($this->dataExport as $key => $title) {
            $returnData[$i][] = $title;
        }
        $i++;

        foreach ($collection as $item) {
            $itemData = $item->getData();
            foreach ($this->dataExport as $key => $title) {
                $returnData[$i][] = (isset($itemData[$key]) ? $itemData[$key] : '');
            }
            $i++;
        }

        return $returnData;
    }
    /**
     * Executed upon customer data deletion.
     *
     * @param CustomerInterface $customer
     *
     * @return void
     * @throws Exception
     */
    public function delete(CustomerInterface $customer)
    {
        $customerId = $customer->getId();
        // Retrieve your customer data from database
        $collection = $this->collectionFactory->create()
            ->addFieldToFilter('customer_id', $customerId);
        if (!$collection->getSize()) {
            return;
        }
        $collection->walk('delete');
    }
    /**
     * Executed upon customer data anonymization.
     *
     * @param CustomerInterface $customer
     *
     * @return void
     * @throws Exception
     */
    public function anonymize(CustomerInterface $customer)
    {
        $customerId = $customer->getId();
        // Retrieve your customer data from database
        $collection = $this->collectionFactory->create()
            ->addFieldToFilter('customer_id', ['eq' => $customerId]);

        $dataAnonymized = $this->customerData->getDataAnonymized($this->dataAnonymize, $customerId);
        if (!empty($dataAnonymized) && $collection->getSize()) {
            $collection > setDataToAll($dataAnonymized)->save();
        }
    }
}

Example

This example is based on Free Plumrocket Twitter & Facebook Login Extension for Magento 2. You can download it from our store for your reference.

Step 1. “di.xml” file of Plumrocket Twitter & Facebook Login Extension

In di.xml file of Plumrocket Twitter & Facebook Login we are specifying which data must be exported and anonymized. While exporting you can define user friendly names for your database fields. In this example, database field “type” will be renamed in CSV file to “Network”, “user_id” will become “User Id”, “image” => “Account Photo Url”, “additional” => “Additional Data”.

File path:

/app/code/Plumrocket/SocialLoginFree/etc/di.xml
<type name="PlumrocketGDPRModelAccountProcessor">
    <arguments>
         <argument name="processors" xsi:type="array">
             <item name="Plumrocket_SocialLoginFree" xsi:type="object">
                 PlumrocketSocialLoginFreeModelGDPRProcessorSocialLogin</item>
         </argument>
    </arguments>
</type>

<type name="PlumrocketSocialLoginFreeModelGDPRProcessorSocialLogin">
    <arguments>
        <argument name="dataExport" xsi:type="array">
            <item name="type" xsi:type="string" translatable="true">Network</item>
            <item name="user_id" xsi:type="string" translatable="true">User Id</item>
            <item name="image" xsi:type="string" translatable="true">
                Account Photo Url</item>
            <item name="additional" xsi:type="string" translatable="true">
                Additional Data</item>
        </argument>
        <argument name="dataAnonymize" xsi:type="array">
            <item name="user_id" xsi:type="string">anonymousString</item>
        </argument>
    </arguments>
</type>

Step 2. GDPR Model of Plumrocket Twitter & Facebook Login Extension

Below is a GDPR Model of Plumrocket Twitter & Facebook Login Extension for Magento 2. Please note that in this example we only delete and export data as per customer request. The process of anonymization is not executed unless you uncomment the code in function “delete(CustomerInterface $customer)”.

File Path:

/app/code/Plumrocket/SocialLoginFree/Model/GDPR/ProcessorSocialLogin.php
<?php

/**
 * Plumrocket Inc.
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the End-user License Agreement
 * that is available through the world-wide-web at this URL:
 * http://wiki.plumrocket.net/wiki/EULA
 * If you are unable to obtain it through the world-wide-web, please
 * send an email to [email protected] so we can send you a copy immediately.
 *
 * @package Plumrocket_SocialLoginFree
 * @copyright Copyright (c) 2018 Plumrocket Inc. (http://www.plumrocket.com)
 * @license http://wiki.plumrocket.net/wiki/EULA End-user License Agreement
 */

namespace PlumrocketSocialLoginFreeModelGDPR;

use PlumrocketGDPRApiDataProcessorInterface;
use PlumrocketGDPRHelperCustomerData;
use PlumrocketSocialLoginFreeModelResourceModelAccountCollectionFactory;
use PlumrocketSocialLoginFreeHelperData;
use MagentoCustomerApiDataCustomerInterface;
use MagentoFrameworkFilesystem;
use MagentoFrameworkFilesystemDriverFile;
use MagentoFrameworkAppFilesystemDirectoryList;

/**
 * Processor SocialLogin.
 */
class ProcessorSocialLogin implements DataProcessorInterface
{

    /**
     * @var CollectionFactory
     */
    protected $collectionFactory;

    /**
     * @var Data
     */
    protected $helper;

    /**
     * @var CustomerData
     */
    protected $customerData;

    /**
     * @var Filesystem
     */
    protected $filesystem;

    /**
     * @var File
     */
    protected $file;

    /**
     * @var array
     */
    protected $dataExport;

    /**
     * @var array
     */
    protected $dataAnonymize;

    /**
     * ProcessorSocialLogin constructor.
     *
     * @param CollectionFactory $collectionFactory
     * @param Data $helper
     * @param array $dataExport
     * @param array $dataAnonymize

     */
    public function __construct(
        CollectionFactory $collectionFactory,
        Data $helper,
        CustomerData $customerData,
        Filesystem $filesystem,
        File $file,
        array $dataExport = [],
        array $dataAnonymize = []

    ) {
        $this->collectionFactory = $collectionFactory;
        $this->helper = $helper;
        $this->customerData = $customerData;
        $this->filesystem = $filesystem;
        $this->file = $file;
        $this->dataExport = $dataExport;
        $this->dataAnonymize = $dataAnonymize;
    }

    /**
     * Executed upon exporting customer data.
     *
     * Expected return structure:
     * array(
     * array('HEADER1', 'HEADER2', 'HEADER3', ...),
     * array('VALUE1', 'VALUE2', 'VALUE3', ...),
     * ...
     * )
     *
     * @param CustomerInterface $customer
     *
     * @return array
     * @throws MagentoFrameworkExceptionNoSuchEntityException
     */
    public function export(CustomerInterface $customer)
    {
        $customerId = $customer->getId();
        $collection = $this->collectionFactory->create()->addFieldToFilter(
            'customer_id', $customerId);
        $returnData = [];
        $i = 0;

        if (!$collection->getSize()) {
            return;
        }

        foreach ($this->dataExport as $key => $title) {
            $returnData[$i][] = $title;
        }
        $i++;

        foreach ($collection as $item) {
            $itemData = $item->getData();
            $itemData['image'] = $this->helper->getPhotoPath(
                false, $customerId, $itemData['type']);

            foreach ($this->dataExport as $key => $title) {
                $returnData[$i][] = (isset($itemData[$key]) ? $itemData[$key] : '');
            }
            $i++;
        }

        return $returnData;
    }

    /**
     * Executed upon customer data deletion.
     *
     * @param CustomerInterface $customer
     *
     * @return void
     * @throws Exception
     */
    public function delete(CustomerInterface $customer)
    {
        // If you wish to anonymize the data instead if deleting it, please uncomment the line below:
        // $this->anonymize($customer);
        // and comment out the rest of the code in this function

        $customerId = $customer->getId();
        $collection = $this->collectionFactory->create()
            ->addFieldToFilter('customer_id', $customerId);
        $mediaRootDir = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)
            ->getAbsolutePath();
        $fileExt = PlumrocketSocialLoginProModelAccount::PHOTO_FILE_EXT;
        $DS = DIRECTORY_SEPARATOR;

        if (!$collection->getSize()) {
            return;
        }

        foreach ($collection as $item) {
            $path = 'pslogin' . $DS . 'photo' . $DS . $item['type'] . $DS .
                $customerId . '.' . $fileExt;
            if ($this->file->isExists($mediaRootDir . $path)) {
                $this->file->deleteFile($mediaRootDir . $path);
            }
        }

        $path = 'pslogin' . $DS . 'photo' . $DS . $customerId . '.' . $fileExt;
        if ($this->file->isExists($mediaRootDir . $path)) {
            $this->file->deleteFile($mediaRootDir . $path);
        }

        $collection->walk('delete');
    }

    /**
     * Executed upon customer data anonymization.
     * This function must be called from the method “delete()” - please read comments above.
     * @param CustomerInterface $customer
     *
     * @return void
     * @throws Exception
     */
    public function anonymize(CustomerInterface $customer)
    {
        $customerId = $customer->getId();
        $collection = $this->collectionFactory->create()->addFieldToFilter(
            'customer_id', ['eq' => $customerId]);
        $dataAnonymized = $this->customerData->getDataAnonymized(
            $this->dataAnonymize, $customerId);
        if (!empty($dataAnonymized) && $collection->getSize()) {
            $collection->setDataToAll($dataAnonymized)->save();
        }
    }
}

GDPR Developer FAQ:

1. How do I change the default Magento 2 GDPR config?

Edit files:

  • app/code/Plumrocket/GDPR/etc/di.xml
  • app/code/Plumrocket/GDPR/Model/Account/Processors/CustomerData.php
  • Or any other file in: app/code/Plumrocket/GDPR/Model/Account/Processors/

2. Can I anonymize all data instead of deleting it?

Yes, you can. Follow these steps:

a) Describe all data that should be anonymized in file di.xml

<type name="PlumrocketGDPRModelAccountProcessorsCustomerData">
    <arguments>
        <argument name="dataExport" xsi:type="array">
            ……. Data export Fields
        </argument>
        <argument name="dataAnonymize" xsi:type="array">
            <item name="prefix" xsi:type="null"/>
            <item name="firstname" xsi:type="string">anonymousString</item>
            …. And other fields
        </argument>
    </arguments>
</type>

b) Edit model PlumrocketGDPRModelAccountProcessorsCustomerData and replace the contents of function delete() with the following code:

public function delete(CustomerInterface $customer)
{
     $this->anonymize($customer);
}

Edit function anonymize() and add the following code:

public function anonymize(CustomerInterface $customer)
{
     $customerId = $customer->getId();
     $customer = $this->customer->load($customerId);
     $dataAnonymized = $this->customerData->getDataAnonymized(
         $this->dataAnonymize, $customerId);
     if (!empty($dataAnonymized) && $customer) {
         $customer->setDataToAll($dataAnonymized)->save();
     }
}

3. I have implemented the GDPR support in my third-party extension, can I share it with you?Yes, absolutely. We can add the built-in support of the third-party extension to our GDPR module after we review your code. Please contact our tech support and submit your request.

4. What data and Magento tables are affected by the GDPR extension?During download, deletion or anonymization process, the following database tables are affected:

Magento 2.x Community Edition (CE)Magento 2.x Enterprise Edition (EE)
customer_entity
customer_entity_*
customer_grid_flat
customer_address_entity
customer_address_entity_*
email_contact
email_review
email_automation
email_campaign
sales_order
sales_order_item
sales_order_address
sales_order_payment
sales_order_grid
sales_shipment_grid
sales_invoice_grid
sales_creditmemo_grid
mage_quote
mage_quote_address
product_alert_price
product_stock_alert
wishlist
review_detail
catalog_compare_item
newsletter_subscriber
plumrocket_gdpr_consents_log
plumrocket_gdpr_export_log
plumrocket_gdpr_removal_requests
customer_entity
customer_entity_*
customer_grid_flat
customer_address_entity
customer_address_entity_*
email_contact
email_review
email_automation
email_campaign
sales_order
sales_order_item
sales_order_address
sales_order_payment
sales_order_grid
sales_shipment_grid
sales_invoice_grid
sales_creditmemo_grid
mage_quote
mage_quote_address
product_alert_price
product_stock_alert
wishlist
review_detail
catalog_compare_item
newsletter_subscriber
magento_customerbalance
magento_reward
magento_rma
magento_rma_grid
magento_invitation
plumrocket_gdpr_consents_log
plumrocket_gdpr_export_log
plumrocket_gdpr_removal_requests
magento_giftregistry_entity
magento_giftregistry_person
magento_sales_order_grid_archive
magento_sales_creditmemo_grid_archive
magento_sales_invoice_grid_archive
magento_sales_shipment_grid_archive

See Magento DevDocs for more information about the Magento 2.x GDPR compliance and data storage.

Use the documentation below to manually place checkboxes in the needed location of the page. At the moment 3 checkbox locations are supported: registration, checkout or newsletter subscription forms. Please note, you must first add the “Consent Checkboxes” in the Magento System Configuration -> GDPR -> Consent Checkboxes. See wiki configuration page for more details.

Example 1. Displaying Checkboxes Using Layout Containers

If your page is built using containers, then you should use the code from the example below.

In this example we will add checkboxes on the Registration Page. Add container to the XML layout file:

Open the file:

/frontend/layout/customer_account_create.xml

and add the code as shown below:

<referenceContainer name="form.additional.info">
    <block class="MagentoFrameworkViewElementTemplate" name="prgdpr_checkbox"
        template="Plumrocket_GDPR::consent-checkboxes-xinit.phtml"
        ifconfig="prgdpr/general/enabled">
        <arguments>
            <argument name="prgdprPage" xsi:type="string">registration</argument>
        </arguments>
    </block>
</referenceContainer>

Where:

“prgdprPage” argument describes the type of the checkbox. E.G:

<argument name="prgdprPage" xsi:type="string">PageType</argument>

“PageType” can have the following values: “registration”, “newsletter” or “checkout”.

Example 2. Displaying Checkboxes Using Layout Blocks

Create layout blocks if you need to add checkboxes within HTML content. Use the code from the example below.

In this example we will add checkboxes into the Newsletter Subscription form.

Step 1. Insert block into the page XML layout

Open the file:

/frontend/layout/default.xml

and add the code as shown below:

<referenceBlock name="form.subscribe">
    <block class="MagentoFrameworkViewElementTemplate"
        name="prgdpr_newsletter_checkbox"
        template="Plumrocket_GDPR::consent-checkboxes-load.phtml"
        ifconfig="prgdpr/general/enabled">
        <arguments>
            <argument name="prgdprPage" xsi:type="string">newsletter</argument>
        </arguments>
    </block>
</referenceBlock>

Step 2. Insert block into the page HTML template

Open the file:

/frontend/templates/subscribe.phtml

and add the highlighted code as shown below:

<div class="field newsletter">
    <label class="label" for="newsletter"><span>
        <?= $block->escapeHtml(__('Sign Up for Our Newsletter:')) ?></span></label>
    <div class="control">
        <input name="email" type="email" id="newsletter"
            placeholder="<?= $block->stripTags(__('Enter your email address')) ?>"
            data-validate="{required:true, 'validate-email':true}"/>
    </div>
</div>

<?= $block->getChildHtml('prgdpr_newsletter_checkbox') ?>

<div class="actions">
    <button class="action subscribe primary"
        title="<?= $block->stripTags(__('Subscribe')) ?>" type="submit">
        <span><?= $block->escapeHtml(__('Subscribe')) ?></span>
    </button>
</div>

Example 3. Displaying Checkboxes Using UI Components

If your page is based on UI Components, then you should use the code from the example below.

Let’s try to add checkboxes on the Checkout Page. Add container and UI Component to the XML layout file:

Open the file:

/frontend/layout/checkout_index_index.xml

and add the code as shown below:

<referenceContainer name="content">
    <block
        class="MagentoFrameworkViewElementTemplate"
        name="prgdpr_checkbox"
        template="Plumrocket_GDPR::consent-checkboxes-jslayout.phtml"
    >
        <arguments>
            <argument name="prgdprPage" xsi:type="string">checkout</argument>
        </arguments>
    </block>
</referenceContainer>

<referenceBlock name="checkout.root">
    <arguments>
        <argument name="jsLayout" xsi:type="array">
            <item name="components" xsi:type="array">
                <item name="checkout" xsi:type="array">
                    <item name="children" xsi:type="array">
                        <item name="steps" xsi:type="array">
                            <item name="children" xsi:type="array">
                                <item name="billing-step" xsi:type="array">
                                    <item name="children" xsi:type="array">
                                        <item name="payment" xsi:type="array">
                                            <item
                                                name="children"
                                                xsi:type="array"
                                            >
                                                <item
                                                    name="payments-list"
                                                    xsi:type="array"
                                                >
                                                    <item
                                                        name="children"
                                                        xsi:type="array"
                                                    >
                                                        <item
                                                            name="before-place-order"
                                                            xsi:type="array"
                                                        >
                                                            <item
                                                                name="children"
                                                                xsi:type="array"
                                                            >
                                                                <item
                                                                    name="prgdpr-consent-checkboxes"
                                                                    xsi:type="array"
                                                                >
                                                                    <item
                                                                        name="component"
                                                                        xsi:type="string"
                                                                        >Plumrocket_GDPR/js/view/consent-checkbox</item
                                                                    >
                                                                    <item
                                                                        name="sortOrder"
                                                                        xsi:type="string"
                                                                        >200</item
                                                                    >
                                                                    <item
                                                                        name="displayArea"
                                                                        xsi:type="string"
                                                                        >before-place-order</item
                                                                    >
                                                                    <item
                                                                        name="dataScope"
                                                                        xsi:type="string"
                                                                        >prgdprConsent</item
                                                                    >
                                                                    <item
                                                                        name="provider"
                                                                        xsi:type="string"
                                                                        >prgdprConsentProvider</item
                                                                    >
                                                                </item>
                                                            </item>
                                                        </item>
                                                    </item>
                                                </item>
                                            </item>
                                        </item>
                                    </item>
                                </item>
                            </item>
                        </item>
                    </item>
                </item>
            </item>
        </argument>
    </arguments>
</referenceBlock>
    

PLEASE NOTE: If the page where you want to display checkboxes is cached with FPC or Varnish Cache, you should use the template “template=”Plumrocket_GDPR::consent-checkboxes-load.phtml”” (shown in examples above) and the checkboxes will be loaded using AJAX. Otherwise use “template=”Plumrocket_GDPR::consent-checkboxes-xinit.phtml”” and the checkboxes will be loaded with page content.

Blocking non-essential Cookies

Plumrocket GDPR extension uses the native Magento “Cookie Restriction Mode” functionality to display Cookie Restriction Notice Block. It allows to block all non-essential cookies until visitor consent is given. Only after the visitor consent is given (eg: “Allow Cookies” button is pressed), the non-essential cookies will be created.

According to GDPR, non-essential cookies, such as those used for analytics, cookies from advertisers or third parties, including affiliates and those that identify a user when he returns to the website should not be used until the consent is provided.

If your JavaScript code creates cookies, you can block it from execution, until the customer consent is given. To do so, copy the code below and insert your JavaScript in the highlighted section:

<script>
    define([
        'jquery',
        'mage/cookies'
    ], function ($) {
        'use strict';

         var allowServices = false,
         allowedCookies,
         allowedWebsites;

         if (<?= (int)$this->helper('Magento/Cookie')
             ->isCookieRestrictionModeEnabled() ?>) {
             allowedCookies = $.mage.cookies.get(
                 '<?= MagentoCookieHelperCookie::IS_USER_ALLOWED_SAVE_COOKIE ?>');

             if (allowedCookies !== null) {
                 allowedWebsites = JSON.parse(allowedCookies);

                 if (allowedWebsites[<?= (int)$block->_storeManager->getWebsite()
                     ->getId() ?>] === 1) {
                     allowServices = true;
                 }
             }
         } else {
             allowServices = true;
         }

         if (allowServices) {
             // Add your js code here
         }
    });
</script>

Developer Guide for GeoIP Functionality

Plumrocket GDPR extension comes bundled with Plumrocket GeoIP Lookup extension. This add-on is used to identify customer’s location from IP and deliver targeted content. With this plugin enabled, GDPR can be configured to display cookie restriction notice and consent checkboxes only for visitors from EU countries or custom list of countries. Learn more about GeoIP Rest API and other methods of retrieving customer location here – Magento GeoIP Lookup Developer Guide and API Reference.

Was this article helpful?