Key takeaways
- The Reporting API uses the same API key as the Offers API, found under Apps, your app, Integration, and it only returns data for that one app.
- GET /v1/stats gives totals grouped by day, country, offer, integration, or sub ID, and GET /v1/conversions lists every transaction with cursor paging.
- Pass an explicit timezone, keep requests under 60 per minute per app, and re-pull recent days because a credited transaction can still be reversed.
What is the Sharklio Reporting API?
The Sharklio Reporting API gives you the numbers from your publisher dashboard as JSON, so you can pull offerwall stats into your own dashboard, spreadsheet, or database. It has two endpoints: GET https://api.sharklio.com/v1/stats for clicks, conversions, and earnings grouped by day, country, offer, integration, or sub ID, and GET https://api.sharklio.com/v1/conversions for every single transaction. You authenticate with your app’s API key in an Authorization: Bearer header, from your server only.
The full reference is the Reporting API docs. To just look at your numbers, use the dashboard: see How to read your dashboard and reports.
Step 1: get your API key
- In your dashboard, open Apps and pick your app.
- Go to the Integration tab. Your API key is there, hidden until you click Show. Click Copy to copy it. The Statistics in your own tools section on the same tab lists both Reporting API endpoints and links to the docs.
- Store the key as an environment variable or secret on your server, for example
SHARKLIO_API_KEY. It starts withshk_.
Four things to know about the key:
- It is the same key as for the Offers API. If you already build your own wall with the Offers API, you have it.
- One key, one app. Each key only returns data for its own app. With several apps, loop over their keys and add up the results yourself. Running several apps from one account explains how apps are split.
- Server side only. Never put the key in a web page, a mobile app, or a public repository. How to keep your app secrets safe covers storage and leaks.
- Replacing it breaks old calls at once. Under Replace your keys, New API key creates a new key and the old one stops working right away, for both the Offers API and the Reporting API. Update every server that uses it.
Reports work for live and paused apps. They are not available for rejected apps or suspended accounts.
Step 2: your first request with curl
With the key in an environment variable, ask for daily stats for a week:
curl "https://api.sharklio.com/v1/stats?from=2026-09-01&to=2026-09-07&group_by=day" \
-H "Authorization: Bearer $SHARKLIO_API_KEY"
Keep the URL in double quotes. The curl manual warns that characters such as & and ? are special to the shell, and an unquoted & cuts your query string short. The -H option adds the header to the request.
A successful answer looks like this (sample response from the Sharklio docs, figures for illustration only):
{
"ok": true,
"from": "2026-09-01",
"to": "2026-09-07",
"timezone": "Europe/Budapest",
"group_by": "day",
"currency": "Coins",
"totals": {
"clicks": 412, "credited": 96, "reversed": 2,
"payout_usd": 61.35, "reward": 39877
},
"rows": [
{
"day": "2026-09-01",
"clicks": 58, "credited": 14, "reversed": 0,
"conversion_rate": 24.14,
"payout_usd": 8.9, "reward": 5785,
"epc_usd": 0.1534
}
]
}
Every answer names its time zone in timezone and your currency name in currency. If you leave out from and to, you get the last 30 days up to today.
The two endpoints at a glance
GET /v1/stats | GET /v1/conversions | |
|---|---|---|
| What you get | Totals plus one row per group | Every transaction, oldest first |
| Main parameters | from, to, timezone, group_by | from, to, timezone, status, user_id, limit, after |
| Grouping or filtering | group_by: day (default), country, offer, integration, sub1 to sub5 | status: credited or reversed (both if left out); user_id for one user |
| Paging | None, one answer | limit 1 to 500 (default 100), then after = next_cursor |
| Best for | Daily charts, country and offer reports, sub ID comparisons | Checking every postback arrived, answering a user’s support request |
What the stats fields mean:
clicks: offers your users started.creditedandreversed: transactions in each state right now. A transaction that was credited and later reversed counts as reversed. Each credited step of a multi-step offer counts.payout_usd: your earnings from credited transactions, in US dollars.reward: what your users received, in your currency.conversion_rate: out of 100 clicks, how many led to at least one credited transaction. A multi-step offer counts once here, so it never goes above 100. It isnullwithout clicks.epc_usd: earnings per click in US dollars, ornullwithout clicks. eCPM and EPC explained shows how to use it.
The totals object has only clicks, credited, reversed, payout_usd, and reward. With group_by=day, every day of the period gets a row, even without activity. Other groups are sorted by earnings, highest first, and offer rows also carry offer_name. The group value is null for traffic without one, for example clicks sent without sub1. To make sub ID reports useful, plan your labels first: What are click IDs and sub IDs? has a simple naming scheme. integration tells you how the user reached the offer: offerwall, api, or sdk.
Step 3: pull yesterday’s stats into a database (Python)
This script stores yesterday’s stats by country and every transaction of that day in a local SQLite file. It needs Python 3.9 or later and the requests package (pip install requests). Run it once a day from cron or any scheduler.
import os
import sqlite3
import time
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import requests
API = "https://api.sharklio.com/v1"
KEY = os.environ["SHARKLIO_API_KEY"]
TZ = "America/New_York" # the zone your reporting day starts in
def call(path, params):
while True:
r = requests.get(f"{API}/{path}", params=params, timeout=30,
headers={"Authorization": f"Bearer {KEY}"})
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "60")))
continue
try:
data = r.json()
except ValueError:
r.raise_for_status()
raise
if not data.get("ok"):
raise RuntimeError("%s %s: %s" % (r.status_code, data.get("error"), data.get("message")))
return data
day = (datetime.now(ZoneInfo(TZ)).date() - timedelta(days=1)).isoformat()
base = {"from": day, "to": day, "timezone": TZ}
db = sqlite3.connect("sharklio.db")
db.execute("""CREATE TABLE IF NOT EXISTS stats_country (
day TEXT, country TEXT, clicks INTEGER, credited INTEGER, reversed INTEGER,
payout_usd REAL, reward REAL, conversion_rate REAL, epc_usd REAL,
PRIMARY KEY (day, country))""")
db.execute("""CREATE TABLE IF NOT EXISTS conversions (
transaction_id TEXT PRIMARY KEY, user_id TEXT, status TEXT, offer_id INTEGER,
offer_name TEXT, reward REAL, payout_usd REAL, country TEXT, device TEXT,
sub1 TEXT, credited_at TEXT, reversed_at TEXT)""")
stats = call("stats", {**base, "group_by": "country"})
for row in stats["rows"]:
db.execute("INSERT OR REPLACE INTO stats_country VALUES (?,?,?,?,?,?,?,?,?)", (
day, row["country"] or "unknown", row["clicks"], row["credited"],
row["reversed"], row["payout_usd"], row["reward"],
row["conversion_rate"], row["epc_usd"]))
after = None
while True:
page = call("conversions", {**base, "limit": 500, "after": after})
for c in page["conversions"]:
db.execute("INSERT OR REPLACE INTO conversions VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", (
c["transaction_id"], c["user_id"], c["status"], c["offer_id"],
c["offer_name"], c["reward"], c["payout_usd"], c["country"],
c["device"], c["sub1"], c["credited_at"], c["reversed_at"]))
after = page["next_cursor"]
if after is None:
break
db.commit()
print(day, stats["timezone"], stats["totals"])
Why it is written this way:
- Yesterday is computed in the report zone. The script works out “yesterday” in
TZwith Python’s zoneinfo module, which supports the IANA time zone database, and sends the same zone astimezone. Your stored day and the API’s day always match. - Parameters go in
params. The Requests quickstart explains thatparamsencodes the query string for you and that keys set toNoneare left out, which is how the first page goes out withoutafter. It also says nearly all production code should set atimeout, or a request can hang forever. - Errors stay readable. The script raises with the API’s stable
errorcode, not a bare HTTP status. transaction_idis the primary key.INSERT OR REPLACEmeans running the script twice never duplicates rows, and a later run updates a transaction that was reversed in the meantime. Values are bound with?placeholders, as the sqlite3 documentation recommends, never pasted into the SQL.
We tested this script against a local mock of the documented responses, including paging, a 429, and a wrong key. For another database, change the two CREATE TABLE statements and the inserts.
Pull yesterday’s stats into Google Sheets
If your reporting lives in a spreadsheet, a Google Apps Script bound to the sheet can append one row per day. Apps Script runs on Google’s servers, not in your users’ browsers. Create a sheet named Sharklio with the headers Day, Clicks, Credited, Reversed, Conversion rate, Payout USD, Reward, and EPC USD, then open Extensions, Apps Script, and paste:
const API = "https://api.sharklio.com/v1/stats";
const TZ = "America/New_York"; // the zone your reporting day starts in
function pullYesterday() {
const key = PropertiesService.getScriptProperties().getProperty("SHARKLIO_API_KEY");
const today = Utilities.formatDate(new Date(), TZ, "yyyy-MM-dd");
const d = new Date(today + "T12:00:00Z");
d.setUTCDate(d.getUTCDate() - 1);
const day = d.toISOString().slice(0, 10);
const query = { from: day, to: day, timezone: TZ, group_by: "day" };
const qs = Object.keys(query)
.map((k) => k + "=" + encodeURIComponent(query[k]))
.join("&");
const res = UrlFetchApp.fetch(API + "?" + qs, {
headers: { Authorization: "Bearer " + key },
muteHttpExceptions: true,
});
const data = JSON.parse(res.getContentText());
if (!data.ok) {
throw new Error(res.getResponseCode() + " " + data.error + ": " + data.message);
}
const row = data.rows[0];
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sharklio").appendRow([
row.day, row.clicks, row.credited, row.reversed,
row.conversion_rate, row.payout_usd, row.reward, row.epc_usd,
]);
}
function createDailyTrigger() {
ScriptApp.newTrigger("pullYesterday")
.timeBased()
.atHour(6)
.everyDays(1)
.inTimezone(TZ)
.create();
}
- Save the key as a script property: click Project Settings, then under Script Properties click Add script property, enter
SHARKLIO_API_KEYand the key, and click Save script properties. Never type it into a cell. Google’s Properties Service guide says script properties are shared by all users of the script, so anyone who can edit the script can read the key. Keep editor access to people who may see it. - Run
pullYesterdayonce from the editor and approve the permissions. - Run
createDailyTriggeronce. Google’s ClockTriggerBuilder reference shows the sameatHour,everyDays, andinTimezonechain; the trigger fires some time within that hour, not on the minute.
muteHttpExceptions: true tells UrlFetchApp not to throw on a failed status, so the script can read the API’s error code. With group_by=day and a one-day range, you always get exactly one row, even on a quiet day.
How dates and time zones work
- The default zone is your account’s. Without
timezone, dates use the zone set under Account, Time zone, the same one your dashboard uses. Sendtimezoneto override it, using an IANA name such asAmerica/New_York,Asia/Kolkata, orUTC. Abbreviations such asESTare rejected withinvalid_timezone, so use the full name. - Always send it in scripts. If someone changes the account time zone later, a script that relies on the default would quietly start storing shifted days.
- Whole days, both included.
fromandtoare dates inYYYY-MM-DDformat, andfrom=2026-10-28&to=2026-10-28is one full day. - Up to 92 days per request. Split longer periods into several requests.
- The credit day decides. A transaction belongs to the day it was credited, even if it was reversed later.
- Timestamps carry their offset.
credited_atandreversed_atare ISO 8601 with the offset, for example2026-09-23T23:59:49+02:00, in the report zone.
Matching your dashboard
If yesterday’s API total differs from the dashboard, check two things first: whether both use the same time zone, and whether a transaction was reversed between the two reads. The API counts each transaction in its current state, so a reversal moves a transaction from credited to reversed on its original day.
Rate limits and caching
The docs set one limit: each app can make 60 reporting requests per minute, separate from the Offers API limit (checked on October 29, 2026). Go over it and you get HTTP 429 with rate_limited; wait for the number of seconds in the Retry-After header, as the Python script does.
The docs do not describe any cache on their side, so plan your own:
- Store, then read your copy. Point your internal dashboard at your database or sheet, not at the API on every page view.
- Today is never final. Numbers for today change as users complete offers. Store closed days, and treat today as a preview.
- Re-pull recent days. A credited transaction can still be reversed later. Each night, re-run the one-day pull for each of the last 7 days (change the
dayline or loop over it), and pullstatus=reversedover a longer window now and then. The docs suggest up to 92 days for reversals, because a reversal can come weeks after the credit. Publisher earnings, settlement, and reversals explains why that happens. - Count your calls. The Python script makes one stats call plus one per 500 transactions, far below the limit.
The same /v1/conversions data also lets you check that every postback reached your server. The docs walk through it under Check that you received every postback: credit by transaction_id, exactly as your postback handler does, so nothing is credited twice. For a single missing one, resend the failed postback from Reports, Postbacks.
Common errors and how to fix them
Errors come as JSON with "ok": false, a stable error code, and a message for your logs. Branch on error, not on the message text.
| HTTP | error | Usual cause and fix |
|---|---|---|
| 400 | invalid_date | A date not in YYYY-MM-DD form, such as 2026-9-1. Pad month and day with zeros. |
| 400 | invalid_range, range_too_long | from after to, or more than 92 days. Swap the dates or split the period. |
| 400 | invalid_group_by, invalid_status, invalid_limit | A value outside the lists above, or a limit outside 1 to 500. |
| 400 | invalid_after, invalid_user_id, invalid_timezone | Send after exactly as the last next_cursor, check the user ID, and use a full IANA zone name. |
| 401 | invalid_api_key | The header is missing or the key is wrong. Send Authorization: Bearer plus the key, and copy it again if it was replaced. |
| 403 | app_not_available | Reports are not available for rejected apps or suspended accounts. |
| 405 | method_not_allowed | Use GET. |
| 429 | rate_limited | Wait for the seconds in Retry-After, then retry. |
Mistakes that give wrong numbers without an error
Calling with the key of a different app (you get that app’s data), adding totals from overlapping date ranges, reading credited as unique users (each step counts), and pulling a day once and never again, so later reversals never reach your records.
Where the Reporting API fits in Sharklio
Sharklio is a pay-per-result affiliate network: your users complete offers on your wall, you earn per completed offer, and a signed postback reaches your server for every credit and every reversal. The dashboard and reports show clicks, conversions, and reversals by country, offer, and sub ID, and the Reporting API puts the same numbers in your own tools. See how the wall works, or get in touch with questions.
Frequently asked questions
Where do I find my Sharklio Reporting API key?
In your dashboard under Apps, your app, Integration. It is the same API key the Offers API uses, and it only returns data for that app.
Can I call the Reporting API from JavaScript in the browser?
No. The docs say to call it from your server only, because the key would be visible to anyone who opens your page. Fetch on your server or in a server-side script and show your users only what you choose.
How much data can I pull in one request?
Up to 92 days per request, with whole days in your chosen time zone. For a longer history, split it into several requests and add up the rows yourself.
How many requests can I make to the Reporting API?
60 reporting requests per minute per app, separate from the Offers API limit. Over that you get a 429 with a Retry-After header telling you how long to wait.
Why do my API numbers differ from my dashboard?
Usually a time zone mismatch or a reversal between the two reads. Send the same timezone your dashboard uses, and remember that a reversed transaction moves from credited to reversed on the day it was first credited.
Can I get stats for all my apps with one key?
No. Each key returns data for its own app only, so loop over each app’s key and combine the results in your database or sheet.



