Troubleshooting Campaign Delivery
The Campaigns API provides visibility into the execution control plane, enabling developers to trace campaign pipeline progress and programmatically diagnose failures at the batch level.
This guide covers how to interpret execution pipeline counts, differentiate between execution and package-level failures, and implement reliable alerting patterns.
Analyzing Pipeline Counts
The counts object returned by GET /campaigns/:id/status and GET /campaigns/:id/metrics maps directly to the stages of the execution pipeline:
{
"counts": {
"scheduled": 17491,
"hydrated": 17491,
"composed": 17485,
"injected": 17485,
"sent": 17480,
"failed": 5
}
}Discrepancies between adjacent pipeline stages indicate specific operational boundaries:
scheduled→hydrated: Represents audience resolution. A variance indicates records filtered out by suppression rules, missing consent, or invalid contact records.hydrated→composed: Represents content rendering. Variances typically involve merge tags referencing null fields or missing template assets causing per-piece composition failures.composed→injected: Represents dispatch preparation. Variances generally point to missing sender credentials, disabled channels, or configuration mismatches.injected→sent: Represents provider acknowledgment. A persistent variance here may indicate that the downstream channel provider has not yet acknowledged the payload or has rejected it asynchronously.failed: The cumulative count of pieces that did not successfully reach thesentstate.
Execution Failures vs. Package Failures
The API exposes two distinct failure surfaces corresponding to different layers of the infrastructure.
Execution Failures (execution.state: "failed")
An execution failure indicates that the run could not progress through the core pipeline. This state is exposed on GET /campaigns/:id/status:
{
"execution": {
"state": "failed",
"lastExecution": {
"status": "failed",
"counts": { "scheduled": 5000, "hydrated": 0, "composed": 0, "injected": 0, "sent": 0, "failed": 5000 }
}
}
}A common pattern is hydrated: 0, which indicates audience resolution failed—typically due to an empty segment, a deleted data schema column, or a missing audience binding.
Package Failures (packageStatus: "failed")
For channels generating execution packages (like Direct Mail), the pipeline may complete but artifact assembly may fail. This state is exposed on GET /campaigns/:id/runs:
{
"runId": "5e5e5e5e-...",
"runStatus": "enqueued",
"packageStatus": "failed",
"package": {
"packageStatus": "failed",
"failureKind": "artifact_write",
"lastErrorCode": "AZURE_BLOB_403",
"lastErrorMessage": "Tenant storage credentials rejected the write."
}
}The failureKind field provides operational categorization:
upstream_execution: The underlying run failed prior to package assembly. Look toexecution.statefor root cause details.finalizer_aggregation: An error occurred while aggregating per-piece manifests. Often transient and safely retriable.artifact_write: The platform encountered an IO or permission error writing to tenant-scoped storage.handoff: The downstream provider integration rejected the assembled package upload.provider: The provider acknowledged the hand-off but reported a subsequent processing failure.
Diagnostic Pattern
When integrating Campaigns API monitoring into operational workflows, monitor both execution status and package processing times.
import os, time, requests
API_KEY = os.environ["EXPERITURE_API_KEY"]
BASE = "https://api.experiture.ai/public/v1"
H = {"Authorization": f"Bearer {API_KEY}"}
def check_campaign_health(campaign_id):
"""Evaluates terminal failures and high failure rates."""
s = requests.get(f"{BASE}/campaigns/{campaign_id}/status", headers=H).json()["data"]
exec_ = s.get("execution", {})
if exec_.get("state") == "failed":
return {
"campaign": campaign_id,
"severity": "high",
"reason": "execution_failed",
"counts": exec_.get("lastExecution", {}).get("counts"),
}
counts = exec_.get("lastExecution", {}).get("counts") or {}
scheduled = counts.get("scheduled", 0)
failed = counts.get("failed", 0)
# Establish an acceptable operational threshold
if scheduled > 0 and (failed / scheduled) > 0.05:
return {
"campaign": campaign_id,
"severity": "medium",
"reason": "elevated_failure_rate",
"rate": failed / scheduled,
"counts": counts,
}
return None
def check_pending_runs(campaign_id, max_age_minutes=30):
"""Detects packages exceeding expected assembly timeframes."""
runs = requests.get(f"{BASE}/campaigns/{campaign_id}/runs?limit=10", headers=H).json()["data"]["runs"]
stuck = [
r for r in runs
if r.get("packageStatus") in ("pending", "assembling")
and minutes_since(r["requestedAt"]) > max_age_minutes
]
if stuck:
return {
"campaign": campaign_id,
"severity": "warn",
"reason": "package_stalled",
"stuck": [{"runId": r["runId"], "status": r["packageStatus"]} for r in stuck],
}
return NoneAlerting Best Practices:
- Minor failure counts are expected (e.g., malformed addresses). Alerting should rely on proportional thresholds rather than absolute failure occurrences.
- Time-based stall detection (
max_age_minutes) is more robust than strict status polling, as large packages may legitimately reside in theassemblingstate during rendering.
Data Privacy and Artifact Classification
Execution artifacts include a piiClassification metadata field specifying data sensitivity levels:
none, contact, phone, address, postal_identity, message_content, tokenized_identifier
This classification supports downstream compliance tools, data warehouses, and audit pipelines by indicating whether an artifact contains sensitive information that requires redaction or secure logging.
Common API Error Codes
Operational API errors use the canonical JOURNEY_API.* namespace.
| Code | Meaning |
|---|---|
JOURNEY_API.PUBLIC.CAMPAIGNS.NOT_DRAFT | Attempted configuration update (PUT) on a published campaign. Trigger new runs via POST /runs. |
JOURNEY_API.PUBLIC.CAMPAIGNS.NOT_FOUND | Campaign identifier is invalid or does not exist within the token's tenant scope. |
JOURNEY_API.CAMPAIGN_RUNS.NOT_PUBLISHED | Attempted POST /runs on a draft. The campaign must be published first. |
JOURNEY_API.CAMPAIGN_RUNS.BACKPRESSURE | The concurrency limit (5 active runs) is reached. Implement backoff and retry. |
JOURNEY_API.BROADCASTS.SCHEDULE.PREFLIGHT_BLOCKERS | Preflight checks rejected the send. Execute POST /campaigns/:id/preflight for the detailed validation report. |
See Also
- Campaign Runs — Run models, status enums, and package objects
- Automating Direct Mail — Direct Mail package handoff and generation
- Recurring & Triggered Campaigns — Implementation strategies for automated operations
- Error Handling & Retries — Comprehensive API retry guidance