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
| Scope | Required for |
|---|---|
campaigns:read | List campaigns, get setup summary, check execution status, run preflight |
analytics:read | Read aggregated metrics (GET /metrics) |
See Authentication to issue a token with these scopes.
The Three Observability Endpoints
| Endpoint | Scope | What it gives you |
|---|---|---|
GET /campaigns/:id/summary | campaigns:read | Setup snapshot — name, channel, audience bindings, template, schedule intent |
GET /campaigns/:id/status | campaigns:read | Execution state machine — scheduled, running, completed, failed + pipeline counts from last run |
GET /campaigns/:id/metrics | analytics:read | Aggregated 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_idFrom 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 summariesThe 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_templateAudience 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:
| State | Meaning |
|---|---|
not_scheduled | No execution history, not scheduled. |
scheduled | Future scheduled send is pending. |
pending_or_overdue | Send time passed but execution has not started yet. |
running | Execution is in progress. |
completed | Execution finished successfully. |
failed | Execution 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:
| Count | Meaning |
|---|---|
scheduled | Records that entered the execution pipeline |
hydrated | Records successfully enriched with profile data |
composed | Records with message content rendered |
injected | Records handed off to the delivery provider |
sent | Records confirmed delivered |
failed | Records 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 resultsSee 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
- Campaigns API Reference — full endpoint contract for all 16 endpoints
- Status & Metrics Reference — response field details
- Preflight — validate campaign readiness before sending
- Authentication —
campaigns:readandanalytics:readscope setup - Rate Limits — quota details and backoff guidance