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

  1. In your dashboard, open Apps and pick your app.
  2. 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.
  3. Store the key as an environment variable or secret on your server, for example SHARKLIO_API_KEY. It starts with shk_.

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/statsGET /v1/conversions
What you getTotals plus one row per groupEvery transaction, oldest first
Main parametersfrom, to, timezone, group_byfrom, to, timezone, status, user_id, limit, after
Grouping or filteringgroup_by: day (default), country, offer, integration, sub1 to sub5status: credited or reversed (both if left out); user_id for one user
PagingNone, one answerlimit 1 to 500 (default 100), then after = next_cursor
Best forDaily charts, country and offer reports, sub ID comparisonsChecking every postback arrived, answering a user’s support request

What the stats fields mean:

  • clicks: offers your users started.
  • credited and reversed: 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 is null without clicks.
  • epc_usd: earnings per click in US dollars, or null without 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 TZ with Python’s zoneinfo module, which supports the IANA time zone database, and sends the same zone as timezone. Your stored day and the API’s day always match.
  • Parameters go in params. The Requests quickstart explains that params encodes the query string for you and that keys set to None are left out, which is how the first page goes out without after. It also says nearly all production code should set a timeout, or a request can hang forever.
  • Errors stay readable. The script raises with the API’s stable error code, not a bare HTTP status.
  • transaction_id is the primary key. INSERT OR REPLACE means 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();
}
  1. Save the key as a script property: click Project Settings, then under Script Properties click Add script property, enter SHARKLIO_API_KEY and 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.
  2. Run pullYesterday once from the editor and approve the permissions.
  3. Run createDailyTrigger once. Google’s ClockTriggerBuilder reference shows the same atHour, everyDays, and inTimezone chain; 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. Send timezone to override it, using an IANA name such as America/New_York, Asia/Kolkata, or UTC. Abbreviations such as EST are rejected with invalid_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. from and to are dates in YYYY-MM-DD format, and from=2026-10-28&to=2026-10-28 is 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_at and reversed_at are ISO 8601 with the offset, for example 2026-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 day line or loop over it), and pull status=reversed over 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.

HTTPerrorUsual cause and fix
400invalid_dateA date not in YYYY-MM-DD form, such as 2026-9-1. Pad month and day with zeros.
400invalid_range, range_too_longfrom after to, or more than 92 days. Swap the dates or split the period.
400invalid_group_by, invalid_status, invalid_limitA value outside the lists above, or a limit outside 1 to 500.
400invalid_after, invalid_user_id, invalid_timezoneSend after exactly as the last next_cursor, check the user ID, and use a full IANA zone name.
401invalid_api_keyThe header is missing or the key is wrong. Send Authorization: Bearer plus the key, and copy it again if it was replaced.
403app_not_availableReports are not available for rejected apps or suspended accounts.
405method_not_allowedUse GET.
429rate_limitedWait 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.