Automating recurring Analytics reports

Set up recurring Analytics reports for your automated workflow with fresh metrics on a schedule. By using the Reporting API with rolling date formulas, a single report request runs without manual modification and delivers current data to your pipeline.

Use automated, recurring report data for the following:

data-variant=info
data-slots=text
This guide is for developers who need Adobe Analytics data delivered to a system on a schedule, with no person in the loop at run time. Such systems include databases, data pipelines, automated workflows, and AI agents. To explore data interactively, build dashboards, or receive statistically-based detection alerts, use Analysis Workspace. This guide covers the programmatic path: scripting the Reporting API to feed current data to the systems that consume it.

The endpoints described in this guide are routed through analytics.adobe.io. To use them, you must first create a client with access to the Adobe Developer Console. For more information, see Getting started with the Analytics API.

If you are new to the Analytics Reporting API, see KPI reports for an introduction to constructing report requests before using this guide. If your use case requires bulk file delivery to cloud storage with Adobe-managed scheduling, see Data Warehouse and Cloud locations APIs instead.

Advantages of automated workflows

When data feeds a system rather than a person, setting up a report pipeline has several advantages. Instead of manually constructing and sharing a scheduled report, you can script a Reporting API call for more control and reliability:

Setting up a recurring report for a pipeline includes the following steps:

  1. Automating token retrieval: Authenticate each scheduled run with a new server-to-server access token

  2. Building the recurring report request: Use date formulas to keep your report current on every scheduled run without modifying the request body

  3. Scheduling the report call: Run the request on a recurring schedule using a Python script and cron

  4. Parsing the JSON response: Parse the JSON response and route the data to your pipeline

The following diagram shows the pipeline flow as outlined above:

flow

Automating token retrieval

By incorporating a job scheduler into a script, you can automate the retrieval of an authorization token. Manual steps in an API client are not required. Each time a scheduler triggers the script, you receive a fresh token and use it immediately for the report request in the same run. Because tokens expire after 24 hours, scripting a scheduled re-fetch at the start of every run is the recommended pattern for recurring jobs.

Your script should make the token authorization call with the following endpoint:

POST https://ims-na1.adobelogin.com/ims/token/v3

Token retrieval request and response examples

Click the Request tab in the following example to see a cURL request for this endpoint. Click the Response tab to see a successful JSON response for the request.

data-slots=heading, code
data-repeat=2
data-languages=CURL,JSON

Request

curl -X POST \
  "https://ims-na1.adobelogin.com/ims/token/v3" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&client_id={CLIENT_ID}&client_secret={CLIENT_SECRET}&scope={SCOPES}"

Response

{
  "access_token": "eyJhbGciOiJSUzI1NiIsIng1dCI6Ik...",
  "token_type": "bearer",
  "expires_in": 86399
}

Request example details

Response example details

Request parameters

Name
Required
Type
Description
grant_type
required
string
The OAuth grant type. Must be client_credentials for server-to-server authentication
client_id
required
string
The Client ID from your Adobe Developer Console OAuth Server-to-Server credential
client_secret
required
string
The Client Secret from your Adobe Developer Console OAuth Server-to-Server credential
scope
required
string
Space-separated list of permission scopes. Required values are listed in your Developer Console project

Response parameters

Name
Type
Description
access_token
string
The bearer token to include as Authorization: Bearer {ACCESS_TOKEN}. Note the space between the word "Bearer" and its value.
token_type
string
The token type
expires_in
integer
Token validity period in seconds

Building the recurring report request

Build a report request that returns the current data window on every scheduled run by using a rolling date formula in the dateRange field. The API evaluates the formula server-side, so the same request body delivers fresh data each time without modification.

If you have worked through the KPI reports guide, the structure of such a request is almost identical, except for the difference in specifying the date range.

To make the request, use the following endpoint:

POST https://analytics.adobe.io/api/{GLOBAL_COMPANY_ID}/reports

Date range formulas

Specify dateRange in globalFilters as a formula string in the format <start>/<end>. Each component combines a base unit representing the current calendar period with an optional shift modifier.

Base units:

Code
Period
th
Current hour
td
Current day
tw
Current week
tm
Current month
tq
Current quarter
ty
Current year

Shift the base unit by appending -Nx or +Nx, where N is the number of periods and x is the unit code. Common formulas for recurring reports:

Formula
Date range
td-7d/td
Last 7 days
td/td+1d
Today
th-24h/th
Last 24 hours
tm-1m/tm
Last month
tq-1q/tq
Last quarter
ty-1y/ty
Last year

Constraints:

Request and response examples

Click the Request tab in the following example to see a cURL request for this endpoint. Click the Response tab to see a successful JSON response for the request.

data-slots=heading, code
data-repeat=2
data-languages=CURL,JSON

Request

curl -X POST \
  "https://analytics.adobe.io/api/{GLOBAL_COMPANY_ID}/reports" \
  -H "accept: application/json" \
  -H "x-api-key: {CLIENT_ID}" \
  -H "Authorization: Bearer {ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
        "rsid": "examplersid",
        "globalFilters": [{"type": "dateRange", "dateRange": "td-7d/td"}],
        "metricContainer": {
          "metrics": [
            {"columnId": "0", "id": "metrics/visits"},
            {"columnId": "1", "id": "metrics/orders"},
            {"columnId": "2", "id": "metrics/revenue", "sort": "desc"}
          ]
        },
        "dimension": "variables/daterangeday",
        "settings": {"countRepeatInstances": true, "limit": 7, "page": 0}
      }'

Response

{
    "totalPages": 1,
    "firstPage": true,
    "lastPage": true,
    "numberOfElements": 7,
    "number": 0,
    "totalElements": 7,
    "columns": {
        "dimension": {
            "id": "variables/daterangeday",
            "type": "time"
        },
        "columnIds": ["0", "1", "2"]
    },
    "rows": [
        {
            "itemId": "1260524",
            "value": "May 24, 2026",
            "data": [682171, 18722, 2288544.73]
        },
        {
            "itemId": "1260523",
            "value": "May 23, 2026",
            "data": [676125, 15219, 2325169.57]
        },
        {
            "itemId": "1260522",
            "value": "May 22, 2026",
            "data": [667478, 19093, 2355620.19]
        }
    ],
    "summaryData": {
        "filteredTotals": [4618723, 134867, 15478399.06],
        "totals": [4618723, 134867, 15478399.06]
    }
}

Request example details

Response example details

Request parameters

Name
Required
Type
Description
rsid
required
string
The report suite ID
globalFilters
required
array
Filter container. For a recurring report, include a single object with type: dateRange and a date formula string
globalFilters[].type
required
string
The filter type. Use dateRange for date range filtering
globalFilters[].dateRange
required
string
The date range for the report. Use a formula string such as td-7d/td to define a rolling window evaluated server-side on every run
metricContainer
required
object
Container for the metrics array
metricContainer.metrics
required
array
List of metrics to include in the report
metricContainer.metrics[].columnId
required
string
Column position in the report, starting from 0. Determines the index of each metric value in rows[].data
metricContainer.metrics[].id
required
string
Metric identifier (e.g., metrics/visits, metrics/orders, metrics/revenue)
metricContainer.metrics[].sort
optional
string
Sort direction for this metric column. Accepts asc or desc
dimension
required
string
The dimension to use for organizing into rows. Use variables/daterangeday for daily breakdowns
settings.countRepeatInstances
optional
boolean
Whether to count repeat instances of a dimension value. Defaults to true
settings.limit
optional
integer
Maximum number of rows to return. Defaults to 50. For a 7-day formula, use 7 to return one row per day
settings.page
optional
integer
Page index for paginated results, starting at 0

Response parameters

Name
Type
Description
totalPages
integer
Total number of pages in the result set
firstPage
boolean
Whether this is the first page of results
lastPage
boolean
Whether this is the last page of results
numberOfElements
integer
Number of rows returned on this page
number
integer
Current page index, starting at 0
totalElements
integer
Total number of rows across all pages
columns.dimension.id
string
The dimension identifier used in the report
columns.columnIds
array
Ordered list of column IDs. Each index maps to the corresponding metric value in rows[].data
rows
array
Report data rows, one entry per dimension value
rows[].itemId
string
Unique identifier for the dimension item
rows[].value
string
Human-readable label for the dimension value (e.g., a formatted date string for time dimensions)
rows[].data
array
Metric values for this row, in the same order as columns.columnIds
summaryData.totals
array
Aggregated metric totals across all rows in the report
summaryData.filteredTotals
array
Aggregated metric totals after any applied filters

Scheduling the report call

Use any external scheduler to run your report script on a recurring interval. The Analytics API has no built-in scheduling requirement. The date formula in the request body determines the data window. Your scheduler determines when the script runs.

In the following example, a Python script is used to combine both API calls from this guide into a single runnable script.

Python script example

import os
import requests

CLIENT_ID = os.environ["ANALYTICS_CLIENT_ID"]
CLIENT_SECRET = os.environ["ANALYTICS_CLIENT_SECRET"]
SCOPES = os.environ["ANALYTICS_SCOPES"]
GLOBAL_COMPANY_ID = os.environ["ANALYTICS_GLOBAL_COMPANY_ID"]
REPORT_SUITE_ID = os.environ["ANALYTICS_REPORT_SUITE_ID"]


def get_access_token():
    response = requests.post(
        "https://ims-na1.adobelogin.com/ims/token/v3",
        data={
            "grant_type": "client_credentials",
            "client_id": CLIENT_ID,
            "client_secret": CLIENT_SECRET,
            "scope": SCOPES,
        },
    )
    response.raise_for_status()
    return response.json()["access_token"]


def run_report(access_token):
    body = {
        "rsid": REPORT_SUITE_ID,
        "globalFilters": [{"type": "dateRange", "dateRange": "td-7d/td"}],
        "metricContainer": {
            "metrics": [
                {"columnId": "0", "id": "metrics/visits"},
                {"columnId": "1", "id": "metrics/orders"},
                {"columnId": "2", "id": "metrics/revenue", "sort": "desc"},
            ]
        },
        "dimension": "variables/daterangeday",
        "settings": {"countRepeatInstances": True, "limit": 7, "page": 0},
    }
    response = requests.post(
        f"https://analytics.adobe.io/api/{GLOBAL_COMPANY_ID}/reports",
        headers={
            "accept": "application/json",
            "x-api-key": CLIENT_ID,
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json",
        },
        json=body,
    )
    response.raise_for_status()
    return response.json()


if __name__ == "__main__":
    token = get_access_token()
    data = run_report(token)
    print(f"Retrieved {len(data['rows'])} rows for the past 7 days")

Example schedule definition

Although schedule definitions vary across schedulers, most schedulers invoke the same script logic on a configured schedule.

In the following example schedule definition, cron is used to run the script daily at 08:00. To use it, open a crontab file with the crontab -e command and type:

0 8 * * * /usr/bin/python3 /opt/scripts/analytics_report.py >> /var/log/analytics_report.log 2>&1
data-variant=info
data-slots=text
The 0 8 * * * prefix is cron schedule syntax. The five fields are minute, hour, day of month, month, and day of week; 0 8 means 8:00, and each * means "every," so this runs at 08:00 every day.

Error handling

Parsing the JSON response

The JSON response can be parsed for the purposes discussed above, including:

Every use case begins by parsing the JSON response into usable records with the metric values, as described in the following sections.

Inherent JSON positional alignment

Parsing the JSON relies upon understanding the inherent positional array alignment in the response. Each entry in the rows array contains one dimension value. For variables/daterangeday, one entry is shown per day, in the order of columns.columnIds, as specified in the request. For example, the Reporting API JSON response example above is already tabular in meaning, by virtue of its rows and column ordering. Taken across all rows, the positional response becomes a simple table of named values:

 date         | visits  | orders | revenue
--------------+---------+--------+-------------
 2026-05-24   |  682171 |  18722 |  2288544.73
 2026-05-23   |  676125 |  15219 |  2325169.57
 2026-05-22   |  667478 |  19093 |  2355620.19

This is the shape the parsing step produces and every downstream use consumes.

The alignment starts in your request, where each metric is assigned a column position:

"metrics": [
    {"columnId": "0", "id": "metrics/visits"},
    {"columnId": "1", "id": "metrics/orders"},
    {"columnId": "2", "id": "metrics/revenue", "sort": "desc"}
]

The response echoes those positions in columns.columnIds:

"columns": {
    "columnIds": ["0", "1", "2"]
}

Each row then returns its metric values in that same order, with no field names:

{
    "itemId": "1260524",
    "value": "May 24, 2026",
    "data": [682171, 18722, 2288544.73]
}

So for May 24, 2026, data[0] is visits, data[1] is orders, and data[2] is revenue. Once parsed, the records are ready for whatever your workflow needs. In some cases, such as a Slack alert or agentic input, you evaluate or pass the parsed values directly, rather than loading them into a destination.

Parsing with the Python standard library

Using the Python standard library, you can reshape each row into a flat record with named fields that any of the use cases above can consume:

records = [
    {
        "date": row["value"],
        "visits": row["data"][0],
        "orders": row["data"][1],
        "revenue": row["data"][2],
    }
    for row in data["rows"]
]

Triggering an alert

For anomaly-based alerting delivered to people, use Anomaly Detection and Intelligent Alerts in Adobe Analytics rather than the API. The API path suits custom, non-statistical thresholds that trigger a system action.

For custom alerting in a pipeline, include scripting logic to evaluate the data rather than load it. Extract the metric value you want to monitor, compare it against a threshold, and act when the condition is met. The following Python shows an example:

todays_revenue = records[0]["revenue"]

if todays_revenue < 2000000:
    send_alert(f"Revenue alert: {todays_revenue} is below the threshold")

The evaluation above is a standard Python conditional. No library or external service is involved. Only the notification (send_alert) reaches an outside service such as a Slack incoming webhook or a paging tool, which is specific to the service you choose and not part of the Analytics API.

If you need only a single value, you can index the response directly instead of building the full record set:

todays_revenue = data["rows"][0]["data"][2]

Supplying input to an agent

This covers scheduled, unattended staging. If an agent needs to fetch data on demand in response to a user, use an MCP server or a direct tool call rather than a scheduled pull.

For agentic use, the parsed records are the input. Because the report returns structured, labeled JSON data, an agent can consume the records as context without additional parsing. If the agent runs on the same schedule as the report, the script can pass the records to it directly. More often, the report and the agent run on different triggers, so the script writes the records to a shared location. This can be a database, cache, or file that the agent reads when it runs. For a database store, see Loading into a database. The following Python shows an example method of writing the records to a shared location:

save_records(records)   # to a store the agent queries on its own trigger

The role of the Reporting API ends at producing current, structured records. The agent data store and retrieval method are part of your agent architecture, not the report request.

Loading into a database

This case suits aggregated report data. For raw, hit-level data or large-volume exports, use Data Feeds or Data Warehouse rather than the Reporting API.

Load the parsed records into your destination table to feed an ETL or Extract, Load, Transform (ELT) pipeline, where each record becomes a row in the table shown above. Because a recurring report runs on a schedule, use an upsert keyed on the date so a repeated run updates the existing row instead of creating a duplicate. The exact statement depends on your database driver and schema.

For example, in PostgreSQL an upsert keyed on the date avoids duplicate rows on repeated runs. Exact syntax varies by database:

INSERT INTO analytics_kpis (date, visits, orders, revenue)
VALUES (%s, %s, %s, %s)
ON CONFLICT (date) DO UPDATE SET
  visits  = EXCLUDED.visits,
  orders  = EXCLUDED.orders,
  revenue = EXCLUDED.revenue;

For a simpler load where duplicate protection is not needed, a data-handling library such as pandas can append the records in a single step:

import pandas as pd

pd.DataFrame(records).to_sql("analytics_kpis", your_engine, if_exists="append", index=False)

Writing to a CSV file

If your pipeline consumes a file rather than a live database connection, write the records to CSV using the standard library:

import csv
from datetime import datetime

run_date = datetime.utcnow().strftime("%Y-%m-%d")

with open(f"analytics_kpis_{run_date}.csv", "w", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=["date", "visits", "orders", "revenue"])
    writer.writeheader()
    writer.writerows(records)

For bulk file delivery to cloud storage with Adobe-managed scheduling, see the Data Warehouse and Cloud Locations guides instead.

Status codes

HTTP code
Meaning
Description
200
Success
The request is successful.
400
Bad Request
The request was improperly constructed, missing key information, and/or contained incorrect syntax.
401
Authentication failed
The request did not pass an authentication check. Your access token may be missing or invalid.
403
Forbidden
The resource was found, but you do not have the right credentials to view it.
404
Not found
The requested resource could not be found on the server.
500
Internal server errors
This is a server-side error. If you are making many simultaneous calls, you may be reaching the API limit and need to filter your results.