Skip to content New SourceLoop MCP: chat with your attribution data in Claude, ChatGPT & Cursor
SourceLoop
API reference

Query

Endpoints under /v1/query. REST endpoints, parameters, and response schemas for the SourceLoop attribution API.

Endpoint Method Path
Run query Run any engine query POST /v1/query

Base URL https://app.sourceloop.ai/api/v1

Run query

Run any engine query

POST /v1/query

Any valid API key. No extra scope needed.

The engine behind the dashboards, docs and MCP: one vocabulary, one set of answers. Send `{ "query": {...} }` for one query or `{ "queries": [{ "id", "query" }] }` for up to 24.

kind `metrics`: `metrics` (each `{ id, as?, events?, types?, stages?, models? }` or `{ formula, as }`), `groupBy` (up to 4), `granularity` (hour to quarter), `compare` (`previous_period`, `previous_year`), `filters` (strings `dim:op:value` or an and/or tree), `sort`, `limit`, `others`. Other kinds: `paths`, `products`, `funnel`, `records`.

Ad platform metrics are ordinary metrics: `spend`, `impressions`, `clicks`, `ctr`, `cpc`, `cpm`, `platform_conversions`, `platform_roas`, and `cost_per_conversion` and `roas`, which take `models` like `conversions` and `revenue` do. Break them down by an ad dimension (`ad_platform`, `ad_campaign`, `ad_set`, `ad`, `keyword`, `ad_search_term`, ...). Rows of an ad dimension show its name, with the platform id in `<dimension>_id`. Spend is converted to the workspace currency at each day's exchange rate: a converted column lists its source currencies in `convertedFrom` and `meta.notes` says so. A day with no published rate stays in the ad account currency, which the column then names in `currency`, and `meta.notes` says when a ratio across currencies cannot be computed.

**Limits.** At most 1,000 rows per query (`limit`, default 50); `meta.truncated` is true when more existed. A window of at most 400 days. At most four `groupBy` dimensions, of which one ad dimension. Ad, cost and profit metrics by day, week, month or quarter, never by hour. Conversions and revenue grouped by an ad dimension need `models`; spend alone needs none. For every ad row by day, page through `GET /ads/stats` instead.

Every answer carries `columns` (what each key means), window `totals` (unique counts are never summed from rows) and `meta` (window, timezone, routes, notes, truncated).

curl -s -X POST "https://app.sourceloop.ai/api/v1/query" \
  -H "Authorization: Bearer $SOURCELOOP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": {
      "kind": "metrics",
      "window": {
        "period": "last 30 days"
      },
      "groupBy": [
        "ad_campaign"
      ],
      "metrics": [
        {
          "id": "spend"
        },
        {
          "id": "conversions",
          "models": [
            "last_non_direct"
          ]
        },
        {
          "id": "roas",
          "models": [
            "last_non_direct"
          ]
        }
      ],
      "sort": [
        {
          "by": "spend",
          "dir": "desc"
        }
      ],
      "limit": 100
    }
  }'
const res = await fetch(
  "https://app.sourceloop.ai/api/v1/query",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SOURCELOOP_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      "query": {
        "kind": "metrics",
        "window": {
          "period": "last 30 days"
        },
        "groupBy": [
          "ad_campaign"
        ],
        "metrics": [
          {
            "id": "spend"
          },
          {
            "id": "conversions",
            "models": [
              "last_non_direct"
            ]
          },
          {
            "id": "roas",
            "models": [
              "last_non_direct"
            ]
          }
        ],
        "sort": [
          {
            "by": "spend",
            "dir": "desc"
          }
        ],
        "limit": 100
      }
    }),
  },
);

const data = await res.json();
import os, requests

res = requests.post(
    "https://app.sourceloop.ai/api/v1/query",
    headers={"Authorization": f"Bearer {os.environ['SOURCELOOP_API_KEY']}"},
    json={
      "query": {
        "kind": "metrics",
        "window": {
          "period": "last 30 days"
        },
        "groupBy": [
          "ad_campaign"
        ],
        "metrics": [
          {
            "id": "spend"
          },
          {
            "id": "conversions",
            "models": [
              "last_non_direct"
            ]
          },
          {
            "id": "roas",
            "models": [
              "last_non_direct"
            ]
          }
        ],
        "sort": [
          {
            "by": "spend",
            "dir": "desc"
          }
        ],
        "limit": 100
      }
    },
)
data = res.json()

Responses

  • 200 Rows, columns, totals and meta
  • 400 Something was wrong with the request or the credential.
  • 429 Too many requests for this workspace in the current minute (`rate-limit-exceeded`), the monthly allowance used (`quota-exceeded`), or too many analytics queries running at once (`too-many-concurrent`). Wait Retry-After seconds and repeat the request unchanged.
  • 503 The analytics store failed or an unexpected error occurred (`upstream-unavailable`). Retry after Retry-After seconds.
  • 504 The query took too long (`upstream-timeout`). Narrow the window or add a filter, then retry.

Track every conversion to its true source

Capture and send full attribution data from every signup, lead, booking, and sale to your CRM and ad platforms, so you know exactly what's driving revenue.

Without SourceLoop

Untagged

Kayden Floyd

kayden@abc.com

  • SourceUnknown
  • MediumUnknown
  • CampaignUnknown
  • Landing pageUnknown
Journey
No touchpoints captured

With SourceLoop

Auto-tagged

Kayden Floyd

kayden@abc.com · Acme Co.

  • Channel Paid Social
  • CampaignFree_demo
  • Landing page/pricing
Journey
Synced to HubSpot Google Ads Meta