# Campaign lifecycle

Immutable plans, bounded budgets, and explicit provider reconciliation.

## Create a plan

POST /api/v1/campaigns requires campaigns:write and Idempotency-Key. The key contains 8–128 letters, digits, dots, colons, underscores, or hyphens. Reusing it with identical normalized input returns the existing campaign; different input returns idempotency_conflict. Keep the key across timeouts.

A live plan requires a ready creative assigned to this owner, the assigned Facebook Page, and a destination within the approved HTTPS origin.

```json
{
  "name": "Bookkeeping launch",
  "url": "https://example.com/pricing",
  "objective": "purchase",
  "budget_cents": 50000,
  "currency": "USD",
  "geo": ["US"],
  "target_cac_cents": 4000,
  "mode": "live",
  "meta": {
    "starts_at": "2030-01-02T12:00:00Z",
    "ends_at": "2030-01-09T12:00:00Z",
    "headline": "Bookkeeping that keeps up",
    "primary_text": "See how it works for your business.",
    "creative_file_id": "file_REPLACE",
    "facebook_social_account_id": "sacc_REPLACE"
  }
}
```

Replace the example dates and resource IDs. Start at least five minutes in the future; duration must be 24 hours to 30 days. Budget bounds are 100–10000000 cents. Geography uses 1–20 uppercase ISO country codes. A supplied conversion_event must match the objective: purchase→purchase, signup→signup, traffic→page_view. target_cac_cents is a planning target, not an enforced acquisition guarantee.

Plans are immutable. A changed destination, creative, copy, schedule, geography, or budget requires a new campaign and approval. The response contains plan_hash, approval_url, mode, status, and provider resource IDs once prepared.

## Prepare, approve, observe

| Local state | Meaning / next action |
| --- | --- |
| awaiting_approval | Stored locally. Live: call prepare. Sandbox: request owner approval. |
| preparing | Provider draft submission in progress or outcome uncertain. Retry the same prepare operation within its retry window. |
| draft | Non-delivering Meta hierarchy exists; owner can inspect the exact plan. |
| launching | Owner approved and budget reserved; activation is in progress or uncertain. Reconcile. |
| active | Configured active or in review. Check delivery_status for actual network delivery. |
| pausing | Pause request in progress or uncertain. Reconcile. |
| paused | the advertising provider confirmed pause. Accrued costs remain payable. |
| attention_required | Provider rejection, mismatch, or another issue requires review. |
| completed | Provider reports completed delivery. |

POST /api/v1/campaigns/{id}/prepare creates one Meta draft, ad group, and ad. It does not enable spending. Owner approval uses a separate endpoint. POST /api/v1/campaigns/{id}/reconcile retrieves current provider state and metrics. GET endpoints return persisted snapshots and do not poll the advertising provider implicitly.

## Pause and retry

POST /api/v1/campaigns/{id}/pause requires campaigns:write. It waits for provider confirmation. A timeout is not a successful pause. Reconcile and retain the original campaign; do not make a duplicate launch. Provider mutations use durable operation keys and no automatic network retries. After 23 hours an ambiguous POST needs operator reconciliation rather than a new idempotency key.

There is no resume or in-place edit operation in this release. Use the revision endpoint below to create a new plan with fresh owner approval. Reserved budget is not automatically released on pause: provider spend can arrive late.

## Read and paginate

GET /api/v1/campaigns?limit=20 returns data.items and data.next_cursor. Pass the returned cursor with the same limit for the next page. Limits are 1–100; order is descending campaign UUID, not chronological. GET /api/v1/campaigns/{id} returns one owner-scoped record; foreign IDs return 404.

## Configure an existing campaign safely

POST /api/v1/campaigns/{id}/revise accepts changes only and requires campaigns:write plus Idempotency-Key. It copies the owned source plan, applies your fields, and validates the complete result. The source stays unchanged. A currently running source keeps running until separately paused.

```bash
curl "$BASE_URL/api/v1/campaigns/$CAMPAIGN_ID/revise" \
  -H "Authorization: Bearer $MR_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: campaign-copy-v2' \
  -d '{"meta":{"headline":"Keep your research together","primary_text":"Organize shared research notes in one workspace. Explore Field Notes."}}'
```

Allowed changes: name, url, objective, budget_cents, geo, target_cac_cents, and partial meta fields (headline, primary_text, creative_file_id, facebook_social_account_id, starts_at, ends_at). Send at least one change. Do not send mode, currency, conversion_event, status, provider IDs, or approval fields. Mode remains the source mode; the conversion event is derived again from the objective. The generated name adds “· revision” unless supplied explicitly.

A live revision must still match the assigned destination and Facebook identity, use a ready owned image, and have a valid future schedule. Update starts_at and ends_at together if the source schedule has already started. To use a generated revision, publish it first, then pass its ready creative_file_id and exact concept copy in meta.

```json
{
  "budget_cents": 15000,
  "meta": {
    "creative_file_id": "file_REPLACE",
    "headline": "Keep your research together",
    "primary_text": "Organize shared research notes in one workspace. Explore Field Notes.",
    "starts_at": "2030-01-05T12:00:00Z",
    "ends_at": "2030-01-08T12:00:00Z"
  }
}
```

Replace the example schedule and file ID. New requests return 201 and an independent campaign; identical retries return 200. The X-Revises-Campaign response header identifies the source. Idempotency is scoped to source campaign and key: retain both after a timeout. No approvals, charges, reservations, or provider resources are copied. Prepare the new live plan, inspect it, and return its new approval_url to the owner. Never infer approval from the source campaign.
