GDPR Data Subject Rights APIs

(updated: January 30, 2020, 3:00PM Pacific)

With Flurry Analytics in the role of Processor with regards to the data covered by GDPR, it is your responsibility to respond to Data Subject Rights (DSR) requests from your users. This documentation describes the usage of the APIs that Flurry makes available for supporting these DSR requests you receive.

Token Creation

As with any Flurry API, in order to use the GDPR APIs, you must first create a Programmatic Token. You can read about how to do that here. The token must be assigned permissions to access Flurry’s GDPR API. To achieve this, after the programmatic account has been created, find the user account on the Admin > Users page and click the pencil icon to edit the user. Then, select Enabled for the GDPR Data Subject Rights API as shown below.

https://s.yimg.com/oo/cms/products/flurry-docs/zh_TW/_images/assign_gdpr_eee24aef3.png

Once you have a Programmatic token that has GDPR API access granted, you can return here to start the integration.

Data Subject Rights

There are 4 DSRs to which this API relates. For each of these DSRs, the following actions will be taken for the device specified in the request:

  • Access - Flurry will return all the data collected

  • Deletion - Flurry will permanently remove all the data collected. Given the permanence of this action, this request has a built in execution delay of 48 hours.

  • Restriction - To service a Restriction DSR, you must first download the subject data via an Access request and then issue a Delete request.

  • Objection - Flurry will permanently remove all the data collected to date and stop the processing of any data sent in the future. As with Deletion, this request has a built in execution delay of 48 hours.

Please Note: In each of the above cases, the APIKey is optional and in its absence data will be returned for all apps to which the Programmatic Token has access.

Other DSRs

There are other DSRs for which Flurry does not provide any support.

  • Rectification - Flurry considers the data collected by your use of Flurry Analytics as “Observed Data”, which is immutable.

API Overview

The GDPR API supports the following use cases:

  1. Initiating a request for data subject rights. This process creates a ticket to record the request and can be monitored for progress and status updates.

  2. Querying the status of an existing ticket. Request status has a simple linear transition between the states of ACKNOWLEDGED, PROCESSING, and COMPLETE.

  3. Downloading client data after a request for Access has successfully completed.

  4. Canceling a request for data erasure. Flurry allows request cancellation during the period of time between request submission (ACKNOWLEDGED) and the beginning of request processing (PROCESSING).

  5. Searching for an existing ticket(s) by submission time, device identifier, and other ticket attributes.

The API is accessible at: https://datarequest.flurry.com/gdpr/v1

API Authentication

The programmatic token should be included inside an Authorization header for all HTTP requests similar to:

Authorization: Bearer *TOKEN*

*TOKEN* should be replaced with the value of the programmatic token.

Initiating Requests

Data subject rights requests to Flurry are initiated by creating a ticket that can be tracked throughout the lifecycle of the request. Developers must provide the following information when creating a ticket:

Ticket Field Name

Description

Required/Optional

ticketType

The data subject right requested. It must have a value (case sensitive) of either:

  • Access

  • Restriction

  • Deletion

  • Objection

Required

deviceIdType

The device identifier type associated with the data subject right request.

It must have a value (case sensitive) of either:

  • IDFA

  • IDFV

  • GAID

  • AndroidId

  • Test

Required

deviceId

The device identifier associated with the data subject right request.

Required

apiKey

If provided, the data subject right request is restricted to the specific project associated with the apiKey. If not provided, the data subject right request is associated with all projects to which the token has access.

Optional

HTTP POST Requests are made against the following URL:

curl -g -X POST -H"Content-Type: application/vnd.api+json" -H"Authorization: Bearer $TOKEN" "https://datarequest.flurry.com/gdpr/v1/ticket"

The entity body should specify the required fields:

{
  "data": {
    "type": "ticket",
    "attributes": {
      "deviceId": "ABCDEF01-0123-ABCD-ABCD-ABCDEF012345",
      "deviceIdType": "IDFV",
      "ticketType": "Objection"
    }
  }
}

When a ticket is generated, the response body includes all attributes from the request including some attributes generated by the API server. These attributes are all permanent members of the ticket and include:

Ticket Field Name

Description

status

The current state of the request ticket. Possible values include:

  • Acknowledged - The initial state of the ticket.

  • Processing - Flurry has begun processing the request. At this point, the ticket can no longer be canceled.

  • Complete - The ticket was successfully completed.

  • NoData - The ticket was successfully completed but no data was found associated with the requested device identifier.

  • Canceled - The ticket was canceled by the developer.

creationDate

The date and time the ticket was created.

modifiedDate

The date and time the ticket was last modified.

downloadURL

If the data subject right includes the ability to download data, a unique URL to fetch from.

id

A unique identifier that can be used to track the status of the ticket.

companyId

Identifies the company associated with this ticket.

A successful response looks like:

< HTTP/1.1 201 CREATED
 < Content-Type: application/vnd.api+json
 < Content-Length: 262
 {
   "data": [
       {
           "attributes": {
               "apiKey": null
               "creationDate": 1522451049000,
               "deviceId": "ABCDEF01-0123-ABCD-ABCD-ABCDEF012345",
               "deviceIdType": "IDFV",
               "downloadUrl": null,
               "modifiedDate": 1522451049000,
               "status": "Processing",
               "ticketType": "Objection",
               "companyId": "686734"
           },
           "id": "3",
           "type": "ticket"
       }
   ]
 }

Monitoring Request Status

After a ticket has been created, it can be monitored by periodically fetching the contents of the ticket and checking the status field. Tickets can be queried individually by specifying the type (ticket) and ticket identifier in the URL path:

curl -H"Authorization: Bearer $TOKEN" "https://datarequest.flurry.com/gdpr/v1/ticket/2"

A successful response looks like:

< HTTP/1.1 200 OK
 < Content-Type: application/vnd.api+json
 < Content-Length: 262
 {
   "data": {
           "attributes": {
               "apiKey": "ABCDEFGHIJKLMNOP",
               "creationDate": 1522451049000,
               "deviceId": "ABCDEF01-0123-ABCD-ABCD-ABCDEF012345",
               "deviceIdType": "IDFV",
               "downloadUrl": null,
               "modifiedDate": 1522451049000,
               "status": "Processing",
               "ticketType": "Objection",
               "companyId": "686734"
           },
           "id": "2",
           "type": "ticket"
       }
 }

Downloading Data

Data will be accessible through a unique, signed URL per ticket. All download URLs expire after seven days. The downloaded data is encoded as a gzip compressed archive (tarball) consisting of two files:

  • data - a JSON encoded text file containing all of the data requested.

  • schema - a JSON schema formatted file which describes the structure of the data file.

It is expected that the schema format will evolve over time. Developers should only use the schema bundled together with the data when attempting decoding.

Example Request

curl "https://flurry-privacy.s3.amazonaws.com/downloads/792.json.gz?AWSAccessKeyId=ABCDEFG&Expires=1522693234&Signature=XYZABC123"

The latest schema can be downloaded here.

Expired URLs

The following error means that the presigned URL has expired:

<Error>
   <Code>AccessDenied</Code>
   <Message>Request has expired</Message>
   <Expires>2017-12-12T17:53:55Z</Expires>
   <ServerTime>2018-01-31T19:47:36Z</ServerTime>
   <RequestId>ABCDEFG</RequestId>
   <HostId>ABC/XYZ/AZ/1234=</HostId>
</Error>

Once expired, the underlying data is deleted and a new ticket must be created to fetch the same data.

Canceling Requests

Developers can cancel requests that have not yet started processing. For requests involving data erasure, Flurry will wait for up to 48 hours to begin processing the request. During this window, it will be possible to cancel requests by updating the status field of the original request ticket to the CANCELED state.

PLEASE NOTE: Once an Erasure request has been processed, it cannot be reversed. Cancellation of an Erasure MUST occur within the 48-hour window.

If Flurry cannot cancel the request because processing has begun, the API will return a 403 error to the caller.

HTTP PATCH Requests are made against a specific ticket by specifying the type (ticket) and ticket identifier in the URL path:

curl -g -X PATCH -H"Content-Type: application/vnd.api+json" -H"Authorization: Bearer $TOKEN" "https://datarequest.flurry.com/gdpr/v1/ticket/2"

The entity body should specify the Canceled status:

{
 "data": {
   "type": "ticket",
   "id": "2",
   "attributes": {
     "status": "Canceled"
   }
 }
}

On successful completion, the server returns:

HTTP/1.1 204 No Content

Searching For Prior Requests

The collection of pre-existing tickets can be searched, sorted, and paginated to aid in retrieving one or more prior requests. Searching and sorting is done against attributes in the ticket including:

  • creationDate

  • modifiedDate

  • status

  • apiKey

  • ticketType

  • deviceIdType

  • deviceId

All search requests are made against the collection of tickets with a base URL of:

https://datarequest.flurry.com/gdpr/v1/ticket

Filtering

The collection of pre-existing tickets is filtered by attaching a *filter[ticket] *query parameter to the request URL. The value of the filter parameter consists of one or more boolean predicates separated by conjunction (logical and) and disjunction (logical or) operators.

Predicates have a simple form: attributeName operator *value *with no intervening whitespace.

Supported Predicate Operator

Description

==

Evaluates to true if the attribute exactly matches the provided value.

=in=

Evaluates to true if the attribute exactly matches any of the provided values. The set of values is enclosed in parentheses and separated by commas.

=out=

Evaluates to true if the attribute does not match any of the values. The set of values is enclosed in parentheses and separated by commas.

==ABC*

Similar to SQL like 'ABC%'.

==*ABC

Similar to SQL like '%ABC'.

==\*ABC\*

Similar to SQL like '%ABC%'.

=lt=

Evaluates to true if the attribute is less than the value.

=gt=

Evaluates to true if the attribute is greater than the value.

=le=

Evaluates to true if the attribute is less than or equal to the value.

=ge=

Evaluates to true if the attribute is greater than or equal to the value.

=isnull=true

Evaluates to true if the attribute is not set.

=isnull=false

Evaluates to true if the attribute is set.

Multiple attribute predicates are separated by AND and OR logical operators (represented as ‘;’ and ‘,’ respectively):

Supported Logical Operator

Description

;

Logical AND

,

Logical OR

The following examples illustrate some common filters:

Example

Explanation

filter[ticket]=deviceId=in=('abc','bcd')

Where deviceId is equal to ‘abc’ OR ‘bcd’

filter[ticket]=modifiedDate=gt=1522450896000

Where the modification date is greater than Friday, March 30, 2018 11:01:36 PM

filter[ticket]=apiKey=isnull=true

Where apiKey is null.

filter[ticket]=ticketType==Objection;apiKey==ABCDEFGHIJKLMNOP

Where the ticket type equals Objection AND the apiey equals ABCDEFGHIJKLMNOP

Sorting

Sorting by any attribute can be specified by the sort HTTP query parameter. The value is the attribute name optionally prepended with a ‘-’ for descending sort order. By default, sorting is ascending. For example:

curl -H"Authorization: Bearer $TOKEN" "https://datarequest.flurry.com/gdpr/v1/ticket?sort=-modifiedDate"

will return:

{
   "data": [
       {
           "attributes": {
               "apiKey": "ABCDEFGHMIJLMNOP",
               "creationDate": 1522451049000,
               "deviceId": "ABCDEF01-0123-ABCD-ABCD-ABCDEF012345",
               "deviceIdType": "IDFV",
               "downloadUrl": null,
               "modifiedDate": 1522451049000,
               "status": "Processing",
               "ticketType": "Objection",
               "companyId": "686734"
           },
           "id": "2",
           "type": "ticket"
       },
       {
           "attributes": {
               "apiKey": null,
               "creationDate": 1522450896000,
               "deviceId": "ABCDEF01-0123-ABCD-ABCD-ABCDEF012345",
               "deviceIdType": "IDFA",
               "downloadUrl": "https://downloads.flurry.com/XYZ",
               "modifiedDate": 1522450896000,
               "status": "Acknowledged",
               "ticketType": "Access",
               "companyId": "686734"
           },
           "id": "1",
           "type": "ticket"
       }
   ]
}

Pagination

The collection of tickets can be paginated by optionally providing any of the three HTTP query parameters:

Query Parameter

Description

Value

page[size]

The total number of tickets to return in the response. The size of the page.

Any positive integer.

page[number]

Which page number to return (starting at 1). Each page has page[size] records.

Any positive integer.

page[totals]

Includes metadata in the response that states the total number of pages and total number of records.

N/A

For example, this request:

curl -H"Authorization: Bearer $TOKEN" "https://datarequest.flurry.com/gdpr/v1/ticket?page[size]=1&page[number]=1&page[totals]"

would return:

{
   "data": [
       {
           "attributes": {
               "apiKey": null,
               "creationDate": 1522450896000,
               "deviceId": "ABCDEF01-0123-ABCD-ABCD-ABCDEF012345",
               "deviceIdType": "IDFA",
               "downloadUrl": "https://downloads.flurry.com/XYZ",
               "modifiedDate": 1522450896000,
               "status": "Acknowledged",
               "ticketType": "Access",
               "companyId": "686734"
           },
           "id": "1",
           "type": "ticket"
       }
   ],
   "meta": {
       "page": {
           "limit": 1,
           "number": 1,
           "totalPages": 2,
           "totalRecords": 2
       }
   }
}

Whenever a pagination parameter is specified, the response includes a meta block that includes information about the current page and/or the total number of records & pages.

Errors

The following HTTP error codes are returned by the API:

HTTP

Error Code Interpretation

200

Returned after a successful HTTP GET to query a ticket.

201

Returned after a successful HTTP POST to create a ticket.

204

Returned after a successful HTTP PATCH to cancel a ticket.

400

The entity body or a query parameter was malformed.

401

The client has invalid, expired, or missing credentials.

403

The client does not have permission to perform the requested operation.

408

The request took too long to complete. For search requests, consider using pagination or filters to restrict the response.

429

The number of requests exceeded the limit allowed for a particular time window. The following headers will be returned with a 429 response:

  • X-RateLimit-Limit: The maximum number of credits that the consumer is permitted to charge. All requests consume at least 1 credit but can cost more depending on the resources consumed.

  • X-RateLimit-Remaining: The number of credits remaining. This number can go negative.

  • X-RateLimit-RefillPerMinute: The rate (credits/minute) at which credits are refilled.

500

The GDPR service had an internal error. The request can be reattempted at a later time/date.

Testing

It is possible to test against the API by creating a ticket with a *Test *deviceIdType. Any string value can be used for the deviceId.

The service will immediately update test tickets to the *COMPLETE *status.

It should also be possible to download test data and the corresponding schema. However, the test data will be generic and not tied to the event model for your application(s).

Errors while downloading the File

Expired Request

If you see a response with the following body, the request needs to be created again using the request api. We normally store contents for upto 7 days:

"data":{
  "attributes": {
      "apiKey": null,
      "creationDate": 1522450896000,
      "deviceId": "ABCDEF01-0123-ABCD-ABCD-ABCDEF012345",
      "deviceIdType": "IDFA",
      "downloadUrl": "Expired URI.  Please resubmit request",
      "modifiedDate": 1522450896000,
      "status": "Complete",
      "ticketType": "Access",
      "companyId": "686734"

  },
  "id": "1",
  "type": "ticket"
}