> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useadmesh.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Reporting

> Retrieve your platform's earnings, exposures, clicks, engagements, and conversions with one API request.

Use the reporting API to bring AdMesh performance numbers into your own dashboard
or reporting workflow. Each request returns totals for your platform over the
selected period. Reports are published in the background approximately once a
minute; the response includes the actual processed cutoff.

```http theme={null}
GET https://api.useadmesh.com/agent/reporting
```

## Get your first report

<Steps>
  <Step title="Get your platform API key">
    Complete [platform onboarding](/platforms/overview), then open
    [API Keys in the AdMesh dashboard](https://useadmesh.com/dashboard/agent/api-keys).
    Use your existing platform API key or generate one there.

    * Use a **production key** for live performance and earnings.
    * Use a **test key** for test activity. Test earnings require recorded test
      accounting and may be unavailable until those records exist.

    Send the key in the `Authorization` header as `Bearer YOUR_API_KEY`.
    The key identifies your platform, so you do not need to supply an agent ID.
    Make reporting requests from your backend using your existing secret storage.
  </Step>

  <Step title="Choose a time period">
    Set the `time_range` query parameter to one of these values:

    | Value | Reporting period |
    | - | - |
    | `today` | Midnight in your configured reporting timezone through the processed cutoff |
    | `1d` | Rolling 24 hours through the processed cutoff |
    | `7d` | Rolling 7 days through the processed cutoff |
    | `30d` | Rolling 30 days; the default if omitted |
    | `90d` | Rolling 90 days |
    | `all` | All recorded history through the processed cutoff |

    `today` follows your platform reporting timezone, defaulting to UTC.
    Rolling windows use UTC and end at the last completed reporting minute.
    The returned `start_at`, `end_at`, and `timezone` describe the actual period.
  </Step>

  <Step title="Request your numbers">
    Replace `YOUR_API_KEY` with your platform's key. This example requests the
    last seven days:

    <CodeGroup>
      ```bash cURL theme={null}
      curl --fail-with-body --include --max-time 30 --request GET \
        'https://api.useadmesh.com/agent/reporting?time_range=7d' \
        --header 'Authorization: Bearer YOUR_API_KEY'
      ```

      ```python Python theme={null}
      import json
      from urllib.parse import urlencode
      from urllib.request import Request, urlopen

      api_key = "YOUR_API_KEY"
      query = urlencode({"time_range": "7d"})
      request = Request(
          f"https://api.useadmesh.com/agent/reporting?{query}",
          headers={"Authorization": f"Bearer {api_key}"},
      )

      # Non-success HTTP responses raise urllib.error.HTTPError.
      with urlopen(request, timeout=30) as response:
          report = json.load(response)
          if response.status == 202:
              retry_after = response.headers.get("Retry-After", "60")
              print(f"Report preparing. Retry after {retry_after} seconds.")
          print(json.dumps(report, indent=2))
      ```

      ```javascript JavaScript theme={null}
      // Node.js 18+; save as reporting.mjs to use top-level await.
      const apiKey = 'YOUR_API_KEY';
      const url = new URL('https://api.useadmesh.com/agent/reporting');
      url.searchParams.set('time_range', '7d');

      const response = await fetch(url, {
        headers: { Authorization: `Bearer ${apiKey}` },
        signal: AbortSignal.timeout(30_000),
      });
      if (!response.ok) {
        throw new Error(`Reporting API returned ${response.status}: ${await response.text()}`);
      }

      const report = await response.json();
      if (response.status === 202) {
        console.log(`Report preparing. Retry after ${response.headers.get('Retry-After') || '60'} seconds.`);
      }
      console.log(report);
      ```

      ```java Java theme={null}
      // Java 11+; save as ReportingExample.java.
      import java.net.URI;
      import java.net.http.HttpClient;
      import java.net.http.HttpRequest;
      import java.net.http.HttpResponse;
      import java.time.Duration;

      public class ReportingExample {
          public static void main(String[] args) throws Exception {
              String apiKey = "YOUR_API_KEY";
              HttpClient client = HttpClient.newBuilder()
                  .connectTimeout(Duration.ofSeconds(10))
                  .build();
              HttpRequest request = HttpRequest.newBuilder()
                  .uri(URI.create("https://api.useadmesh.com/agent/reporting?time_range=7d"))
                  .timeout(Duration.ofSeconds(30))
                  .header("Authorization", "Bearer " + apiKey)
                  .GET()
                  .build();
              HttpResponse<String> response = client.send(
                  request, HttpResponse.BodyHandlers.ofString());
              if (response.statusCode() != 200 && response.statusCode() != 202) {
                  throw new IllegalStateException(
                      "Reporting API returned " + response.statusCode() + ": " + response.body());
              }
              if (response.statusCode() == 202) {
                  String retryAfter = response.headers().firstValue("Retry-After").orElse("60");
                  System.out.println("Report preparing. Retry after " + retryAfter + " seconds.");
              }
              System.out.println(response.body());
          }
      }
      ```

      ```go Go theme={null}
      package main

      import (
          "fmt"
          "io"
          "log"
          "net/http"
          "time"
      )

      func main() {
          apiKey := "YOUR_API_KEY"
          client := &http.Client{Timeout: 30 * time.Second}
          request, err := http.NewRequest(http.MethodGet,
              "https://api.useadmesh.com/agent/reporting?time_range=7d", nil)
          if err != nil {
              log.Fatal(err)
          }
          request.Header.Set("Authorization", "Bearer " + apiKey)

          response, err := client.Do(request)
          if err != nil {
              log.Fatal(err)
          }
          defer response.Body.Close()
          body, err := io.ReadAll(response.Body)
          if err != nil {
              log.Fatal(err)
          }
          if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusAccepted {
              log.Fatalf("Reporting API returned %d: %s", response.StatusCode, body)
          }
          if response.StatusCode == http.StatusAccepted {
              retryAfter := response.Header.Get("Retry-After")
              if retryAfter == "" {
                  retryAfter = "60"
              }
              fmt.Printf("Report preparing. Retry after %s seconds.\n", retryAfter)
          }
          fmt.Println(string(body))
      }
      ```
    </CodeGroup>

    These examples use Python 3, Node.js 18+, Java 11+, or Go 1.16+ and their
    standard HTTP libraries; no AdMesh SDK is required. Change `7d` to any of the
    supported periods. The examples print the JSON response so you can choose
    which fields to display in your dashboard.

    For HTTP `202`, wait for the `Retry-After` interval before requesting the
    report again. That response contains preparation metadata, not metric totals.
    The cURL example includes response headers so you can read this interval.

    A completed response contains totals and the selected period:

    ```json theme={null}
    {
      "agent_id": "your-agent-id",
      "time_range": "7d",
      "start_at": "2026-10-10T12:00:00Z",
      "end_at": "2026-10-17T12:00:00Z",
      "currency": "USD",
      "is_test": false,
      "report_schema_version": 2,
      "earnings_basis": "ledger",
      "status": "ready",
      "earnings_coverage": "complete",
      "processed_through": "2026-10-17T12:00:00Z",
      "updated_at": "2026-10-17T12:00:30Z",
      "accounting_reconciled_through": "2026-10-17T12:00:00Z",
      "timezone": "UTC",
      "exposures": 1000,
      "clicks": 80,
      "engagements": 20,
      "conversions": 5,
      "earnings_micros": 12500000,
      "earnings_excluded_recommendations": 0,
      "pending_earnings_recommendations": 0,
      "accounting_review_required": 0,
      "earnings_adapter_version": "recorded_earnings_v1"
    }
    ```

    Completed empty periods return zero counts and earnings. For `all`,
    `start_at` is `null`. During initial preparation, HTTP `202` returns metadata
    only, with `status: "preparing"` and `Retry-After: 60`. Retry after that interval;
    missing numbers do not mean zero activity.
  </Step>

  <Step title="Display earnings and performance">
    Read `exposures`, `clicks`, `engagements`, and `conversions` directly.
    Divide `earnings_micros` by **1,000,000** to display earnings in USD:

    ```javascript theme={null}
    const earningsUsd = report.earnings_micros / 1_000_000;
    // 12500000 micros = $12.50

    const formattedEarnings = new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: report.currency,
    }).format(earningsUsd);
    ```

    Change `time_range` and make another request when a user selects a different
    period in your dashboard.
  </Step>
</Steps>

## What the numbers mean

| Field | Meaning |
| - | - |
| `exposures` | Unique recommendations shown, including recommendations that were later clicked, engaged, or converted. Mentioned recommendations also count. Generated-only recommendations do not count. |
| `clicks` | Recommendations currently marked clicked, engaged, or converted, matching AdMesh recommendation reports. |
| `engagements` | Recommendations currently marked engaged. |
| `conversions` | Recommendations currently marked converted. |
| `earnings_micros` | Recorded net earnings from accounting, in millionths of a USD. Missing accounting amounts are not estimated from bid prices. |
| `earnings_excluded_recommendations` | Recommendations whose earnings were excluded because their currency is foreign or unresolved. Their activity remains in the counts. |
| `is_test` | `true` for test traffic and recorded test earnings; `false` for live traffic. |
| `start_at`, `end_at` | UTC reporting boundaries, with an inclusive start and exclusive end. |

<Note>
  Earnings can include unsettled amounts. They represent performance earnings,
  not your withdrawable balance or a payout statement. See [Payouts](/platforms/payouts)
  to set up your bank details and track payments.
</Note>

## Report freshness and earnings completeness

`status` is `preparing`, `ready`, or `stale`. After preparation, the API retains
the last completed report if processing fails. Reports more than two minutes
behind the processing cutoff are marked stale. Check `processed_through` and
`updated_at` before displaying the numbers as current.

`earnings_coverage` is independently `complete`, `partial`, or `unavailable`.
A ready report can have incomplete earnings. Check
`pending_earnings_recommendations` for activity awaiting accounting records and
`earnings_excluded_recommendations` for foreign or unresolved currency.
`accounting_review_required` flags accounting rows that cannot be attributed.
`accounting_reconciled_through` is the accounting reconciliation watermark, or
`null` if no completed reconciliation is known. Earnings coverage also remains
partial or unavailable while accounting reconciliation is behind the reporting
cutoff. Do not interpret missing earnings as a verified zero.

## Set your reporting timezone

Your platform owner can set an IANA timezone through the existing onboarding
settings endpoint. This settings request uses the owner's signed-in **Firebase ID
token**, while reporting requests continue to use the platform API key.

```bash theme={null}
curl --fail-with-body --request PATCH \
  'https://api.useadmesh.com/agent/update-onboarding' \
  --header 'Authorization: Bearer YOUR_FIREBASE_ID_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{"reporting_timezone":"America/New_York"}'
```

The change appears in the next completed report. Today respects local daylight
saving transitions; rolling 1/7/30/90-day windows remain UTC durations.

## How periods and counts work

The time filter uses **recommendation creation time**. Later clicks, engagements,
or conversions update the period in which the recommendation was created.
Historical totals can therefore change as those outcomes arrive.

Each recommendation counts once at its highest engagement status. These are
recommendation counts, not counts of individual event callbacks. The combined
`clicks` definition is not a count of billable CPC events and never controls
settlement. For example, a
converted recommendation counts as an exposure, click, and conversion; it is no
longer counted as an engagement even if it was engaged earlier.

The API returns only totals. `time_range` is the only supported query parameter;
custom dates, other agent IDs, traffic overrides, and recommendation lists are
not supported. Your production key reports live traffic only, while your test
key reports test traffic only.

## Troubleshooting

| Status or result | Next step |
| - | - |
| `401` | Check that your API key is valid and sent as `Authorization: Bearer YOUR_API_KEY`. |
| `403` | Your platform account is unavailable. Contact AdMesh support. |
| `422` | Use a supported `time_range` and remove any other query parameters. |
| `202` | The first report is preparing. Retry after the `Retry-After` interval. |
| `503` | Reporting is temporarily unavailable. Retry later. |
| Report is stale | Display the processing cutoff and retry later. Contact support if freshness does not recover. |
| Earnings are partial or unavailable | Review coverage fields; activity can arrive before its accounting records. |
| All numbers are zero | Check the selected period and whether you are using a test or production key. Only shown recommendations count. |
| Earnings exclusions are greater than zero | Some amounts could not be included in USD earnings. Contact support if you need help reconciling them. |

Need help? Contact [support@useadmesh.com](mailto:support@useadmesh.com).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.