Reports API quickstart
Pull a saved analytics report into your own script or workflow. One key, one POST, JSON back.
You need one saved report first. Open your website in Hitsteps, go to Reports, save the report you want to read, then open it and choose Share and automate → Reports API to create a key. The full reference with every date rule, field and error code is the Reports API guide.
English · curl, Python and n8n · Updated November 6, 2026
The request shape
Every example calls the same endpoint with the same two fields, so once you see one, the rest are variations:
POST https://www.hitsteps.com/api/reports-v2.php
Authorization: Bearer YOUR_REPORT_API_KEY
Content-Type: application/json
{"from": 1793829600, "until": 1793916000}
from is the first account-local day you want, included. until is the start of the day after the last day you want, excluded. Both are integer Unix timestamps in seconds, never milliseconds and never calendar strings. The two fields replace the saved report's date range completely; only the comparison setting comes from the saved report, so there is nothing else to send.
A window can span at most 31 days and never includes today: the current day is never served, since it is not complete. Asking for a window that includes today, starts before your retained history, or exceeds 31 days returns 422 report_unavailable, not 400.
The example above is the 2026-11-05 day for an account at UTC+02:00. Run the samples with your own key and your own complete, kept days; the cURL and Python ones below compute the previous complete account-local day for you.
cURL
Store the key in your server environment as HITSTEPS_REPORT_API_KEY, then run one command. The script computes the previous complete day in your account's timezone first, so you never hardcode timestamps. The shell's built-in printf passes the header through standard input so the key never appears in cURL's process arguments, and --include shows Retry-After if you ever get rate-limited.
#!/bin/sh
# Previous complete account-local day for an account at UTC+02:00.
OFFSET=7200 # your Hitsteps account's UTC offset in seconds
UNTIL=$(( ( $(date +%s) + OFFSET ) / 86400 * 86400 - OFFSET ))
FROM=$(( UNTIL - 86400 ))
printf 'Authorization: Bearer %s\n' "$HITSTEPS_REPORT_API_KEY" |
curl --silent --show-error --include \
'https://www.hitsteps.com/api/reports-v2.php' \
-H @- \
-H 'Content-Type: application/json' \
--data "{\"from\":$FROM,\"until\":$UNTIL}"
An HTTP error can still carry a JSON body, so check the status line too, not just whether cURL connected.
Python
This example asks for the previous complete day in the account's own timezone, which keeps your schedule aligned with the dashboard. Set the offset to your account's fixed UTC offset. It needs requests; metric values come back as strings, so the sample converts them before printing.
import os
import time
import requests
key = os.environ["HITSTEPS_REPORT_API_KEY"]
offset_seconds = 2 * 3600 # your account's UTC offset in seconds (UTC+02:00 here)
now = int(time.time())
until = (now + offset_seconds) // 86400 * 86400 - offset_seconds # start of today, account-local
start = until - 86400 # the previous complete account-local day
response = requests.post(
"https://www.hitsteps.com/api/reports-v2.php",
headers={"Authorization": "Bearer " + key},
json={"from": start, "until": until}, # requests sets Content-Type: application/json
timeout=30,
)
try:
data = response.json()
except ValueError:
data = {}
if response.status_code != 200 or not data.get("ok"):
retry = response.headers.get("Retry-After")
hint = f", retry after {retry} s" if retry else ""
raise SystemExit(f"Reports API returned HTTP {response.status_code}: {data.get('error', 'no JSON body')}{hint}")
result = data["result"]
print(f"{result['current']['from_date']}: {len(result['rows'])} rows, totals {result['totals']}")
for row in result["rows"]:
page = row["dimension_1"].get("value", "") # Pages report broken down by page URL
print(int(row["pageviews"]), page) # metric values are strings: convert before maths
Keep the key out of your code and out of version control. Read it from the environment or a secret manager, as above, and give the key a short expiry so rotation stays easy.
n8n
The built-in HTTP Request node can call the Reports API as-is, no plugin to install. This reads a report each run and makes the day boundary reusable. (There is no separate Hitsteps node for n8n; this is all native n8n.)
- Add an HTTP Request node. Under Authentication, add a credential of type Header Auth with Name
Authorizationand ValueBearer <your key>. The key lives in the credential, not in the workflow. - Set the node to POST and the URL to
https://www.hitsteps.com/api/reports-v2.php. - Make the two timestamps easy to reuse. A small Code node before the request returns the previous complete account-local day in seconds:
// Code node: previous complete day for an account at UTC+02:00
const offsetSeconds = 2 * 3600;
const now = Math.floor(Date.now() / 1000);
const until = Math.floor((now + offsetSeconds) / 86400) * 86400 - offsetSeconds;
return [{ json: { from: until - 86400, until: until } }];
- In the HTTP Request node set the body to JSON using the two values:
{"from": {{ $json.from }}, "until": {{ $json.until }}}. n8n adds theContent-Type: application/jsonheader itself for a JSON body, so no extra header step is needed. - Downstream nodes read the report through
{{ $json.result.rows }}and{{ $json.result.totals }}. The request returns a single item, so you can branch, filter or load it into whatever runs next.
The same three-node workflow is available as an import-ready JSON file below: paste it into n8n's Clipboard import dialog, then create a Header Auth credential named Hitsteps Reports API key yourself and run it.
Import-ready n8n workflow (JSON)
{
"id": "hitstepsReportsQuickstart",
"name": "Hitsteps Reports API: previous day",
"active": false,
"nodes": [
{
"id": "a1",
"name": "Run once",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"parameters": {}
},
{
"id": "a2",
"name": "Previous day",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [220, 0],
"parameters": {
"jsCode": "// Code node: previous complete day for an account at UTC+02:00\nconst offsetSeconds = 2 * 3600;\nconst now = Math.floor(Date.now() / 1000);\nconst until = Math.floor((now + offsetSeconds) / 86400) * 86400 - offsetSeconds;\nreturn [{ json: { from: until - 86400, until: until } }];"
}
},
{
"id": "a3",
"name": "Hitsteps report",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [440, 0],
"parameters": {
"method": "POST",
"url": "https://www.hitsteps.com/api/reports-v2.php",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\"from\": {{ $json.from }}, \"until\": {{ $json.until }}}",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"id": "hitstepsReportKey",
"name": "Hitsteps Reports API key"
}
}
}
],
"connections": {
"Run once": { "main": [[{"node": "Previous day", "type": "main", "index": 0}]] },
"Previous day": { "main": [[{"node": "Hitsteps report", "type": "main", "index": 0}]] }
},
"settings": { "executionOrder": "v1" },
"pinData": {}
}Results for the same report and days are cached for up to an hour, so schedule runs at most every 15 minutes. The default limits are 6 requests per minute and 60 per hour per key, and 240 per hour per website across all of its keys. One workflow that fetches a report each day uses a tiny fraction of that.
The JSON you get back
A successful response is HTTP 200 with the rows under result.rows and the period totals under result.totals. An illustrative response for a Pages report, values are examples, not customer data:
{
"ok": true,
"version": 2,
"report_id": 103,
"report_revision": 1,
"result": {
"dataset": "pages",
"timezone_offset": 3600,
"current": {
"from_date": "2026-11-05",
"until_date": "2026-11-06",
"days": 1
},
"rows": [
{
"dimension_1": {"id": "page.url", "value": "/pricing", "state": "set"},
"pageviews": "120",
"unique_visitors": "38"
}
],
"totals": {"pageviews": "120", "unique_visitors": "38"},
"warnings": [],
"has_more": false
}
}
result.currentshows which days were actually read, in your account's local dates. Every window that includes today or starts before your retained history answers422 report_unavailable, so none of it is served.- Empty
rowsmeans the window found no matching data on those days. That is a success, not a failure. - Metric values in
rowsandtotalsare exact decimal strings ornull, not numbers. Convert them before doing maths:int(row["pageviews"])in Python,Number()in JavaScript. Do not treatnullas zero. - An error comes back as
{"ok":false,"error":"code"}with an HTTP status. The common ones are400 invalid_request,401 unauthorized,403 not_available,413 request_too_largewhen the body exceeds 4096 bytes,415 unsupported_media_typewhen Content-Type is not application/json,422 report_unavailable,429 rate_limitedwithRetry-After, and503 temporarily_unavailable. The guide explains each one and how to respond.
Next step
Open your saved report in Hitsteps, create a Reports API key, and run the cURL example with your own key and days. Once you see your numbers as JSON, the Python and n8n versions are simply the same call from the place where your automation lives.
Using the older website-level Analytics API instead? Its keys and request format are different, see the Analytics API reference.