---
title: REST API
description: Read your reporting data over HTTPS — authentication, conventions, endpoints and error handling.
updated: 2026-07-30
order: 3
related: [/reference/mcp, /reference/postback, /concepts/vocabulary]
---

The API returns your own campaign data as JSON, from the same queries that back the
dashboard — so the API and the UI always agree.

The authoritative, machine-readable definition is the
[OpenAPI specification](https://api.postbackx.com/v1/openapi.json). Most HTTP clients
and code generators can consume it directly. This page is the orientation.

```
https://api.postbackx.com/v1
```

## Authentication

Create a key on [API & MCP](https://app.postbackx.com/developers), then send it as a
bearer token:

```bash
curl -s "https://api.postbackx.com/v1/reports/totals?start_date=2026-07-01&end_date=2026-07-20" \
  -H "Authorization: Bearer pbx_live_YOUR_KEY"
```

Keys are read-only and bound to a single organization. If your account belongs to
several, a key reads the one it was issued for; to read another, issue a key there.

Keep keys on a server or in a scheduled job. Never in browser code, and never in a
committed file.

## Conventions

Worth reading once — they explain most first-attempt failures.

- **Dates are required.** Every reporting endpoint needs an explicit `start_date` and
  `end_date`. There is no default window.
- **Ranges are inclusive** on both ends, and formatted `YYYY-MM-DD`.
- **Time zones are yours to choose.** Pass an IANA name as `timezone`; it defaults to
  UTC, which is probably not how you read your own reports.
- **Lists are comma-separated strings**, e.g. `campaign_ids=9c1f...,4ab2...`.
- **Payloads are wrapped** in `data`, with context under `meta`.
- **Metrics are pre-computed.** `roas`, `conversion_rate`, `epc` and `cpa` come back
  calculated. Recomputing them from the raw fields is how a report starts disagreeing
  with the dashboard.
- **Rate limit is 1000 requests per hour** per credential.

## What is there

**Reporting** — `/reports/totals` for one headline row, `/reports/timeseries` for a
trend, `/reports/campaigns` and `/reports/offers` for per-entity rows,
`/reports/breakdown` for the ad-platform hierarchy down to the creative, and
`/reports/calls` for inbound call attribution.

**Entities** — `/campaigns`, `/offers`, `/offer-sources`, `/tracking-domains`,
`/organizations`. Use these to turn the ids that reporting returns into names.

**Raw data** — `/clicks` and `/conversions`, one row per record. These are the heavy
ones: page through them and store the results rather than re-pulling the same window.

**Writes** — `PATCH /campaigns/{id}` and `PATCH /offers/{id}`, accepting `status` only
(`active`, `paused` or `archived`). That is deliberately the entire write surface, so
an automation can pause a loser without any risk of rewriting its configuration.

> Conversions cannot be created through this API. They arrive at the
> [postback URL](/reference/postback), which also handles deduplication, click
> matching and delivery to the ad platforms — a row written directly would reach none
> of those.

The full parameter list for each endpoint is in the
[specification](https://api.postbackx.com/v1/openapi.json) and in the searchable
explorer on [API & MCP](https://app.postbackx.com/developers).

## Reading the breakdown endpoint

`/reports/breakdown` is the one with a shape worth explaining, because it returns the
whole tree at once rather than one level per call.

Every row carries its own `level`, `entity_key` and `parent_key`. You rebuild the
hierarchy by matching a row's `parent_key` to the `entity_key` above it. Pass `level`
to get a single rung instead of everything.

Level names are platform-specific, because each platform names its own hierarchy
(`adset` on Facebook, `tt_adgroup` on TikTok). The cross-platform shorthands
`campaign`, `adgroup` and `creative` fan out across all of them, and are what you want
unless you are targeting one platform.

## Errors

Every failure uses one envelope:

```json
{
  "error": {
    "type": "validation_failed",
    "message": "One or more parameters were rejected.",
    "fields": [
      { "field": "start_date", "code": "required", "message": "start_date is required." }
    ]
  }
}
```

**Branch on `error.type`, never on the message.** Messages are written for people and
will be reworded; the types are the contract.

| Type | Status | What to do |
| --- | --- | --- |
| `validation_failed` | 422 | Read `error.fields`; each names the parameter and why. |
| `unauthorized` | 401 | The key is missing, malformed or revoked. |
| `forbidden` | 403 | Valid key, wrong organization. |
| `not_found` | 404 | No such endpoint or record. |
| `rate_limited` | 429 | Back off for `error.retry_after_seconds`. |
| `internal_error` | 500, 502 | Our side. Retry with exponential backoff. |

## Not writing code

If the goal is answers rather than a integration, connect an
[AI assistant](/reference/mcp) instead — same data, no client to maintain. The app's
[API & MCP](https://app.postbackx.com/developers) page also has ready-made snippets
for Google Sheets, Python, Make and Zapier.
