Guides
Campaign Reporting & Observability

Campaign Reporting & Observability

Pull live campaign state from Experiture into external systems — BI dashboards, data warehouses, ops tools, or custom monitoring. Three dedicated read endpoints cover the full observability surface of any campaign.

When to use this:

  • Feed campaign pipeline counts into a data warehouse or reporting layer
  • Monitor active campaign execution from an ops dashboard
  • Verify campaign configuration readiness before triggering a send
  • Build an internal campaign status board alongside CRM records

Prerequisites

ScopeRequired for
campaigns:readList campaigns, get setup summary, check execution status, run preflight
analytics:readRead aggregated metrics (GET /metrics)

See Authentication to issue a token with these scopes.


The Three Observability Endpoints

EndpointScopeWhat it gives you
GET /campaigns/:id/summarycampaigns:readSetup snapshot — name, channel, audience bindings, template, schedule intent
GET /campaigns/:id/statuscampaigns:readExecution state machine — scheduled, running, completed, failed + pipeline counts from last run
GET /campaigns/:id/metricsanalytics:readAggregated pipeline counts — for BI, reporting, and dashboards

Use /summary for configuration review. Use /status for ops monitoring. Use /metrics for reporting integrations.


Getting Campaign IDs

From the Console

The campaign_id is a UUID visible in the Experiture console campaign detail URL:

https://app.experiture.ai/campaigns/4130bada-9264-465f-bc0c-a26bebcfcc81/...
                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                                    this is your campaign_id

From the API

Use GET /campaigns to discover and list campaigns programmatically:

# List published email campaigns, most recent first
curl "https://api.experiture.ai/public/v1/campaigns?status=published&channel=email&limit=50" \
  -H "Authorization: Bearer <your_access_token>"
import requests
 
def list_published_campaigns(token: str, channel: str = None) -> list[dict]:
    params = {"status": "published", "limit": 100, "sortBy": "updatedAt", "sortDirection": "desc"}
    if channel:
        params["channel"] = channel
 
    r = requests.get(
        "https://api.experiture.ai/public/v1/campaigns",
        params=params,
        headers={"Authorization": f"Bearer {token}"},
    )
    r.raise_for_status()
    body = r.json()
    return body["data"]   # list of campaign summaries

The list endpoint uses offset pagination. Walk pages with:

offset, total = 0, None
while total is None or offset < total:
    r = requests.get(url, params={**params, "offset": offset}, headers=headers)
    body = r.json()
    total = body["pagination"]["total"]
    offset += body["pagination"]["limit"]
    # process body["data"]...

Common Patterns

1 — Verify Campaign Configuration (Setup Summary)

GET /campaigns/:id/summary returns a structured setup snapshot. Use it to confirm audience, template, and schedule are all configured before you trigger a send or build a status display.

curl https://api.experiture.ai/public/v1/campaigns/4130bada-9264-465f-bc0c-a26bebcfcc81/summary \
  -H "Authorization: Bearer <your_access_token>"
{
  "success": true,
  "data": {
    "campaignId": "4130bada-9264-465f-bc0c-a26bebcfcc81",
    "name": "Spring Re-engagement Direct Mail",
    "campaignType": "broadcast",
    "channel": "direct_mail",
    "authoringStatus": "draft",
    "createdAt": "2026-04-25T10:00:00.000Z",
    "updatedAt": "2026-04-25T10:30:00.000Z",
    "schedule": {
      "mode": "at",
      "sendAt": "2026-05-01T09:00:00.000Z",
      "timezone": "America/New_York",
      "scheduled": true
    },
    "audience": {
      "included": [
        { "id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "name": null, "type": "segment", "memberCount": null }
      ],
      "excluded": [],
      "reach": {
        "status": "ok",
        "evaluatedCount": 18432,
        "evaluatedAt": "2026-04-25T10:20:00.000Z",
        "isStale": false
      }
    },
    "content": {
      "template": {
        "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
        "name": "Spring DM v3",
        "status": "published",
        "channel": "direct_mail"
      },
      "proofStatus": "unavailable"
    },
    "preflight": {
      "status": "not_evaluated",
      "checks": []
    },
    "latestExecution": {
      "status": "not_scheduled",
      "lastExecutionAt": null,
      "counts": null
    },
    "links": {
      "self": "/public/v1/campaigns/4130bada-9264-465f-bc0c-a26bebcfcc81/summary"
    }
  }
}

Check configuration readiness:

import requests
 
def is_campaign_configured(campaign_id: str, token: str) -> bool:
    r = requests.get(
        f"https://api.experiture.ai/public/v1/campaigns/{campaign_id}/summary",
        headers={"Authorization": f"Bearer {token}"},
    )
    r.raise_for_status()
    data = r.json()["data"]
    has_audience = len(data["audience"]["included"]) > 0
    has_template = data["content"]["template"] is not None
    return has_audience and has_template

Audience member counts are intentionally redacted in public summaries. Use audience.reach.evaluatedCount for the latest reach evaluation and /status after a campaign runs to see actual execution counts.


2 — Monitor Execution State

GET /campaigns/:id/status gives you the execution state machine plus pipeline counts from the most recent run.

curl https://api.experiture.ai/public/v1/campaigns/4130bada-9264-465f-bc0c-a26bebcfcc81/status \
  -H "Authorization: Bearer <your_access_token>"
{
  "success": true,
  "data": {
    "campaignId": "4130bada-9264-465f-bc0c-a26bebcfcc81",
    "name": "Spring Re-engagement — Direct Mail",
    "authoringStatus": "published",
    "schedule": {
      "scheduleId": "sch-00000000-0000-0000-0000-000000000001",
      "sendAt": "2026-05-01T09:00:00.000Z",
      "timezone": "America/New_York",
      "active": false
    },
    "execution": {
      "state": "completed",
      "lastExecution": {
        "executionId": "exec-00000000-0000-0000-0000-000000000001",
        "startedAt": "2026-05-01T09:00:05.000Z",
        "completedAt": "2026-05-01T09:47:22.000Z",
        "status": "completed",
        "counts": {
          "scheduled": 17491,
          "hydrated": 17491,
          "composed": 17485,
          "injected": 17485,
          "sent": 17480,
          "failed": 5
        }
      }
    }
  }
}

Poll until execution completes:

import requests, time
 
TERMINAL_STATES = {"completed", "failed"}
 
def wait_for_execution(campaign_id: str, token: str, poll_interval: float = 10.0, max_polls: int = 60) -> str:
    """
    Polls GET /status until execution reaches a terminal state.
    Returns the final execution.state value.
    """
    for attempt in range(max_polls):
        r = requests.get(
            f"https://api.experiture.ai/public/v1/campaigns/{campaign_id}/status",
            headers={"Authorization": f"Bearer {token}"},
        )
        r.raise_for_status()
        data = r.json()["data"]
        state = data["execution"]["state"]
 
        print(f"[{attempt + 1}/{max_polls}] state: {state}")
        if state in TERMINAL_STATES:
            return state
 
        time.sleep(poll_interval)
 
    raise TimeoutError(f"Campaign {campaign_id} did not reach terminal state after {max_polls} polls")

execution.state values:

StateMeaning
not_scheduledNo execution history, not scheduled.
scheduledFuture scheduled send is pending.
pending_or_overdueSend time passed but execution has not started yet.
runningExecution is in progress.
completedExecution finished successfully.
failedExecution finished with errors.

3 — Extract Pipeline Counts for Reporting

GET /campaigns/:id/metrics returns aggregated pipeline counts scoped to analytics:read. Use this for data warehouse ingestion and BI tools.

import requests
from datetime import datetime, timezone
 
def get_metrics_row(campaign_id: str, token: str) -> dict:
    """
    Returns a flat dict suitable for loading into a data warehouse or BI table.
    Requires analytics:read scope.
    """
    r = requests.get(
        f"https://api.experiture.ai/public/v1/campaigns/{campaign_id}/metrics",
        headers={"Authorization": f"Bearer {token}"},
    )
    r.raise_for_status()
    data = r.json()["data"]
    counts = data["counts"]
 
    return {
        "campaign_id": data["campaignId"],
        "pulled_at": datetime.now(timezone.utc).isoformat(),
        **counts,  # scheduled, hydrated, composed, injected, sent, failed
    }

Pipeline counts explained:

CountMeaning
scheduledRecords that entered the execution pipeline
hydratedRecords successfully enriched with profile data
composedRecords with message content rendered
injectedRecords handed off to the delivery provider
sentRecords confirmed delivered
failedRecords that dropped out at any stage

A healthy send has sent ≈ scheduled. A significant gap between injected and sent usually indicates a provider-side delivery issue.


4 — Pull Metrics for Multiple Campaigns

import requests, time
 
def pull_all_metrics(token: str, channel: str = None) -> list[dict]:
    """
    Pages through all published campaigns and pulls metrics for each.
    Requires campaigns:read + analytics:read.
    """
    base = "https://api.experiture.ai/public/v1"
    headers = {"Authorization": f"Bearer {token}"}
 
    # Step 1: collect all published campaign IDs
    campaign_ids = []
    params = {"status": "published", "limit": 100}
    if channel:
        params["channel"] = channel
 
    offset, total = 0, None
    while total is None or offset < total:
        r = requests.get(f"{base}/campaigns", params={**params, "offset": offset}, headers=headers)
        r.raise_for_status()
        body = r.json()
        total = body["pagination"]["total"]
        campaign_ids.extend(c["campaignId"] for c in body["data"])
        offset += body["pagination"]["limit"]
 
    # Step 2: fetch metrics for each
    results = []
    for cid in campaign_ids:
        r = requests.get(f"{base}/campaigns/{cid}/metrics", headers=headers)
        if r.status_code in (404, 409):
            continue  # deleted or unsupported type
        r.raise_for_status()
        results.append(r.json()["data"])
        time.sleep(0.05)  # stay within rate limits
 
    return results

See Rate Limits for per-scope quota details.


Handling Unsupported Campaign Types

If you call a public v1 Campaigns endpoint against a campaign that is not a broadcast, you receive 409, not 404. The campaign exists and your token is valid; the public v1 dispatcher simply does not expose that campaign type yet. Summary/object projection uses SUMMARY_TYPE_UNSUPPORTED; action/status/metrics operations use TYPE_UNSUPPORTED.

import requests
 
def fetch_summary_safe(campaign_id: str, token: str):
    r = requests.get(
        f"https://api.experiture.ai/public/v1/campaigns/{campaign_id}/summary",
        headers={"Authorization": f"Bearer {token}"},
    )
    if r.status_code == 409:
        error = r.json().get("error", {})
        campaign_type = error.get("details", {}).get("campaignType", "unknown")
        print(f"Summary not available for campaign type: {campaign_type}")
        return None
    r.raise_for_status()
    return r.json()["data"]

See Also