# Reporting API

> Pull your clicks, conversions, and earnings into your own dashboards, and check every transaction against the postbacks you received.

Source: https://docs.moonetix.com/reporting-api

The Reporting API gives you the numbers from your dashboard as JSON: statistics for any period, grouped the way you need, and the full list of transactions. Use it to build your own reports, to compare traffic sources, and to check that you received a postback for every transaction.

## Authentication

Use the same API key as for the [Offers API](https://docs.moonetix.com/offers-api). You find it in your dashboard under Apps, your app, Integration. Send it in the `Authorization` header:

```
Authorization: Bearer shk_your_api_key
```

Call the API from your server only. Each key only returns data of its own app. Reports work for live and paused apps.

## Dates and time zone

All dates and times are in the time zone of your account, the same as in your dashboard (Account, Time zone), or in the zone you send as `timezone`. Every answer names its zone in `timezone`. `from` and `to` are whole days in `YYYY-MM-DD` format, and both are included. Without them you get the last 30 days up to today. One request can cover up to 92 days: split longer periods into several requests.

A transaction belongs to the day it was credited, even if it was reversed later. Timestamps in responses are ISO 8601 with the offset, for example `2026-09-23T23:59:49+02:00`.

## Statistics

`GET https://api.moonetix.com/v1/stats`

| Query parameter | Description |
| --- | --- |
| `from` (optional, date, YYYY-MM-DD) | First day, in the report time zone. Defaults to 29 days before `to`. |
| `to` (optional, date, YYYY-MM-DD) | Last day, in the report time zone. Defaults to today. |
| `timezone` (optional, string) | An IANA time zone for the dates and times, for example `America/New_York`, `Asia/Kolkata`, or `UTC`. Defaults to the time zone of your account. |
| `group_by` (optional, string) | `day` (default), `country`, `offer`, `integration`, or `sub1` to `sub5`. `integration` is `offerwall`, `api`, or `sdk`: how the user reached the offer. With `day`, every day of the period has a row, even without activity. Other groups are sorted by earnings, highest first. |

Every row has these fields. The `totals` object has only the counts (`clicks`, `credited`, `reversed`), `payout_usd`, and `reward`.

| Field | Description |
| --- | --- |
| `day`, `country`, `offer`, `integration`, or `sub1` to `sub5` | The group of the row, named after `group_by`. It is `null` for traffic without a value, for example clicks without `sub1`. With `offer`, the row also has `offer_name`. |
| `clicks` | Offers your users started. |
| `credited`, `reversed` | Transactions in each state right now. A transaction that was credited and later reversed counts as reversed. |
| `payout_usd` | Your earnings from credited transactions, in US dollars. |
| `reward` | What your users received from credited transactions, in your currency. |
| `conversion_rate` | Out of 100 clicks, how many led to at least one credited transaction, or `null` without clicks. A multi-step offer counts once, however many of its steps were credited, so this never goes above 100. `credited` still counts every credited step. |
| `epc_usd` | Earnings per click in US dollars, or `null` without clicks. |

## Conversions

`GET https://api.moonetix.com/v1/conversions`

Every transaction of the period, oldest first, with the same values you receive in postbacks.

| Query parameter | Description |
| --- | --- |
| `from`, `to` (optional, date, YYYY-MM-DD) | The period, in the report time zone, as for statistics. |
| `timezone` (optional, string) | An IANA time zone for the dates and times, for example `America/New_York`, `Asia/Kolkata`, or `UTC`. Defaults to the time zone of your account. |
| `status` (optional, string) | `credited` or `reversed`. Leave it out to get both. |
| `user_id` (optional, string) | Only transactions of this user, for example to answer a support request. |
| `limit` (optional, integer, 1 to 500) | Transactions per page. Defaults to 100. |
| `after` (optional, string) | The `next_cursor` of the previous page. Leave it out for the first page. |

```json
{
  "ok": true,
  "from": "2026-09-01",
  "to": "2026-09-07",
  "timezone": "Europe/Budapest",
  "currency": "Coins",
  "next_cursor": "1103",
  "conversions": [
    {
      "transaction_id": "9ca45437948f42022b8118b0",
      "user_id": "user_1234",
      "status": "credited",
      "offer_id": 120,
      "offer_name": "Coin Quest",
      "reward": 146,
      "payout_usd": 0.225,
      "country": "DE",
      "device": "android",
      "sub1": "homepage", "sub2": null, "sub3": null, "sub4": null, "sub5": null,
      "credited_at": "2026-09-03T23:59:49+02:00",
      "reversed_at": null
    }
  ]
}
```

When `next_cursor` is not `null`, there are more transactions: send the same request again with `after` set to that value.

## Check that you received every postback

Postbacks are retried for hours, but a server outage on your side can still make you miss one. Once a day, compare your records with the API:

1. Request the last 7 days with `status=credited`, and page through all results. A transaction is listed under the day it was credited, so this also covers days when your check did not run.
2. For every `transaction_id` you have not credited yet, credit the user with `reward`.
3. Do the same with `status=reversed` for transactions you credited but have not taken back. A reversal is listed under the day the transaction was credited and can come weeks later, so check reversals over a longer window, for example 92 days.

Use `transaction_id` as the unique key, exactly as in your postback handler, so a transaction is never credited twice. You can also resend a failed postback from your dashboard under Reports, Postbacks.

## Errors

Errors come as JSON with `"ok": false`, a stable `error` code, and a `message` for your logs.

| HTTP | error | What to do |
| --- | --- | --- |
| 400 | `invalid_date`, `invalid_range`, `range_too_long`, `invalid_group_by`, `invalid_status`, `invalid_limit`, `invalid_after`, `invalid_user_id`, `invalid_timezone` | Fix the parameter named in the code. |
| 401 | `invalid_api_key` | Send the key in the `Authorization` header, and copy it again if you replaced it. |
| 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 number of seconds in the `Retry-After` header. |

## Limits

- Each app can make 60 reporting requests per minute. This limit is separate from the Offers API.
- Numbers for today change as users complete offers. A credited transaction can still be reversed later, and then counts as reversed.

## Quick reference

### Request

`GET api.moonetix.com/v1/stats`

```bash
curl "https://api.moonetix.com/v1/stats?from=2026-09-01&to=2026-09-07&group_by=day" \
  -H "Authorization: Bearer $MOONETIX_API_KEY"
```

```php
$url = 'https://api.moonetix.com/v1/stats?' . http_build_query([
    'from' => '2026-09-01', 'to' => '2026-09-07', 'group_by' => 'day',
]);
$ctx = stream_context_create(['http' => [
    'header' => 'Authorization: Bearer ' . getenv('MOONETIX_API_KEY'),
]]);
$data = json_decode(file_get_contents($url, false, $ctx), true);
```

```javascript
const params = new URLSearchParams({ from: '2026-09-01', to: '2026-09-07', group_by: 'day' });
const res = await fetch(`https://api.moonetix.com/v1/stats?${params}`, {
  headers: { Authorization: `Bearer ${process.env.MOONETIX_API_KEY}` },
});
const data = await res.json();
```

```python
r = requests.get(
    "https://api.moonetix.com/v1/stats",
    params={"from": "2026-09-01", "to": "2026-09-07", "group_by": "day"},
    headers={"Authorization": f"Bearer {api_key}"},
)
data = r.json()
```

### Response 200

```json
{
  "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
    }
  ]
}
```
