Recast Knowledge Base
Breadcrumbs

The Plans API

The Plans API lets you programmatically read the Plans you've built in the Recast app and get the budgets, configuration, and forecasted performance from each one. It mirrors what you see in the Plans tab of the app.

Note that this is a read-only API. There is no create or update endpoint yet. If you want to create a new Plan you can follow the instructions here to create one in the UI that you can read using the API.

See the Swagger documentation for full schema details.

Concepts

Term

Meaning

Plan

A saved marketing plan. This is a budget plus configuration (spikes, contextual variables, lower funnel settings) over a date range, used to forecast and optimize performance.

Plan Version

A snapshot of a Plan at a point in time. Every edit creates a new version. Only the primary version is live and refreshed weekly with new data.

Primary version

The current, editable version of a Plan. This is tagged with primary_version = true

Plan type

default : auto-generated by Recast, refreshed with every model update, assumes future spend follows historical patterns, covers 730 days. custom : user-built with custom spend and spike specifications.

Status

This marks whether the current date falls within a Plans budget range. โ€œfutureโ€ Plans start in the future, โ€œcurrentโ€ Plans span the current date, and โ€œexpiredโ€ Plans are entirely in the past.

Budget (summary)

The channel "legend" for a version. This shows which spend channels, non-spend channels, contextual variables, and lower funnel channels each version uses. Returned by the version show endpoint.

Budget (data table)

The full budget associated with a Plan version as a CSV.

Lower funnel channel caps

Shows how the spend in your lower funnel channel is planned. Note that the API returns โ€œmanualโ€ for lower funnel channels where spend is labeled as โ€œprovidedโ€ in the app.

Spikes / depvar spike groups

Promotions and holidays included in the plan, grouped by name, with the affected depvars (dependent variables) and dates.

Compatible / incompatible KPIs

Whether a given KPI can be forecast from this plan version's inputs. A KPI becomes incompatible when the plan is missing a channel, spike, or contextual variable the model needs.

Counterfactual (altcast)

A retroactive forecast of days that have already elapsed, using the plan's current model rather than whatever model existed at the time. This is different from the plan's regular forecast, which looks forward. The altcast_type field takes one of three values. It is null for the plan's regular forecast. It is "planned" when the current model is predicting the outcome of the plan version's originally specified budget. It is "actuals" when the current model is predicting the outcome of the actual spend that occurred.

Recommendations

Suggested budget reallocations to improve a forecasted outcome, available at Conservative / Moderate / Aggressive risk levels. This is available on all Forecasts associated with the Plan.

Adherence

A comparison of the spend you planned against the spend that actually happened, channel by channel. This is what the Adherence section of the Plans tab shows. Adherence is computed per Plan version and refreshed as new spend data arrives.

Report-through date

The last date covered by an Adherence run (report_through_date). Both the planned and the actual spend are reported through this date, so the two are directly comparable.

Start from the Index to find the Plan you care about (or its id from the Plans tab URL), then drill into its versions. Most of the time you want the primary version which is available directly in the Plans Index.

1. GET /plans                                          โ†’ find the plan, note primary_version.id
2. GET /plans/{plan_id}/versions/{version_id}           โ†’ full config for that version
3. GET /plans/{plan_id}/versions/{version_id}/budget    โ†’ the daily budget as CSV

Use the versions list when you need a history of edits to your Plan (e.g. comparing what changed between two versions of a plan).

Forecasts and Adherence require just the Plan id rather than the version id:

GET /plans/{plan_id}/forecasts?plan_version_id={version_id}     โ†’ forecasted performance
GET /plans/{plan_id}/adherence?plan_version_id={version_id}    โ†’ planned vs. actual spend

The Endpoints

1. Index GET /v1/clients/{client_slug}/plans

Returns every Plan visible in the UI with summary information about the primary version of each Plan.

Query parameters (all optional):

Param

Type

Description

page

integer

Default 1

per_page

integer

Default 25, max 100

plan_type

string

Filter to default or custom

label

string

Partial, case-insensitive match on the plan's label

status

string

Filter to one or more of expired, current, future (comma-separated for multiple, e.g. status=current,expired)

kpi_id

string (UUID)

Filter to plans whose primary version can forecast this KPI

created_by

string

Filter to plans created by a specific user (email), or Recast for system-generated plans

Response

JSON
{
  "data": [
    {
      "id": "8d777078-044e-4e3d-8d95-8dc14daf8d93",
      "label": "Q3 Growth Plan",
      "plan_type": "custom",
      "status": "current",
      "start_date": "2026-07-01",
      "end_date": "2026-09-30",
      "created_by": "user@example.com",
      "primary_version": {
        "id": "4b026308-cce1-42f0-a1ac-2b6b902766ec",
        "label": "Plan Version 14 2026-06-01 15:30:05",
        "plan_version_id": "v3",
        "primary": true,
        "total_spend": 1250000.0,
        "created_by": "user@example.com"
      },
      "created_at": "2026-05-01T12:00:00Z",
      "updated_at": "2026-06-10T09:30:00Z"
    }
  ],
  "pagination": { "page": 1, "per_page": 25, "total_pages": 1, "total_count": 3 }
}


2. Version list GET /v1/clients/{client_slug}/plans/{plan_id}/versions

This endpoint returns all versions of one Plan, ordered by creation date descending.

Response

JSON
{
  "data": [
    {
      "id": "4b026308-cce1-42f0-a1ac-2b6b902766ec",
      "plan_version_id": "v3",
      "label": "Plan Version 14 2026-06-01 15:30:05",
      "plan_id": "8d777078-044e-4e3d-8d95-8dc14daf8d93",
      "primary": true,
      "total_spend": 1250000.0,
      "created_by": "user@example.com",
      "created_at": "2026-05-01T12:00:00Z",
      "updated_at": "2026-06-10T09:30:00Z"
    }
  ]
}

Exactly one version per plan has primary: true.

3. Version show GET /v1/clients/{client_slug}/plans/{plan_id}/versions/{id}

This endpoint returns the full Plan summary for one version of a plan. You will need to provide client_slug, plan_id, and the version id to specify the version you want to return.

Response

JSON
{
  "id": "4b026308-cce1-42f0-a1ac-2b6b902766ec",
  "plan_version_id": "v3",
  "label": "Plan Version 14 2026-06-01 15:30:05",
  "plan_id": "8d777078-044e-4e3d-8d95-8dc14daf8d93",
  "primary": true,
  "status": "current",
  "start_date": "2026-07-01",
  "end_date": "2026-09-30",
  "created_by": "user@example.com",
  "total_spend": 1250000.0,
  "spike_type": "custom",
  "budget_summary": {
    "spend_channels": ["meta", "google", "tiktok"],
    "non_spend_channels": ["organic"],
    "contextual_variables": ["holiday", "promo"],
    "lower_funnel_channels": ["branded_search"]
  },
  "lower_funnel_channel_caps": [
    { "channel_name": "branded_search", "option": "capped", "cap": 50000.0 }
  ],
  "depvar_spike_groups": [
    {
      "spike_name": "Black Friday",
      "depvars": [
        { "depvar_slug": "total_sales", "dates": ["2026-11-27"] }
      ]
    }
  ],
  "compatible_kpis": [
    { "id": "d66d6adb-1d17-40cc-9bc7-f4bc6c78ec2e", "slug": "walmart_revenue", "label": "Walmart Revenue" }
  ],
  "incompatible_kpis": [],
  "created_at": "2026-05-01T12:00:00Z",
  "updated_at": "2026-06-10T09:30:00Z"
}

Note: budget_summary here shows only the channels that are included in the plan not the daily spend. Use the budget endpoint below to get the complete budget for a Plan version.

4. Version budget GET /v1/clients/{client_slug}/plans/{plan_id}/versions/{id}/budget

Returns the full daily budget data table for a version as a CSV.

The first column is date. The remaining columns are the channels listed in the version's budget_summary (spend_channels + non_spend_channels + contextual_variables + lower_funnel_channels).

date,meta,google,tiktok,holiday,promo
2026-07-01,1000.0,2000.0,500.0,0,1
2026-07-02,1100.0,2100.0,550.0,0,1

5. Forecasts & Counterfactuals GET /v1/clients/{client_slug}/plans/{plan_id}/versions/{version_id}/forecasts

Plan-driven forecasts are surfaced as a nested resource under the plan version. Each entry is a normal Forecast result (see the Forecaster API), tagged with the plan it belongs to.

  • GET /plans/{plan_id}/forecasts shows forecasts across every version of the plan. Pass planned or actuals into altcast_types to get the counterfactual forecasts.

  • GET /plans/{plan_id}/forecasts/{forecast_id} shows a single forecast from a particular Plan version

  • GET /plans/{plan_id}/forecasts/{forecast_id}/downloads/{key}: downloads a CSV for one of the forecast's results.

Here is an example index response.

{
  "data": [
    {
      "id": 321,
      "name": "Holiday spend forecast",
      "status": "success",
      "altcast_type": null,
      "plan_version_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "plan_version_number": "v3",
      "kpi_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "kpi_label": "Walmart Revenue",
      "created_at": "2026-01-15T09:24:00Z",
      "updated_at": "2026-01-15T09:31:00Z"
    }
  ],
  "pagination": { "page": 1, "per_page": 25, "total_pages": 4, "total_count": 87 }
}

Here is an example show response.

{
  "id": 321,
  "name": "Holiday spend forecast",
  "status": "success",
  "altcast_type": null,
  "start_date": "2026-07-05",
  "end_date": "2026-07-31",
  "plan_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "plan_label": "Q3 Growth Plan",
  "plan_version_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "plan_version_number": "v3",
  "kpi_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "kpi_label": "Walmart Revenue",
  "form": {
    "budget": [
      ["date", "facebook", "google"],
      ["2026-12-01", "10000", "15000"],
      ["2026-12-02", "10000", "15000"]
    ],
    "custom_model_type": "roi",
    "start_date": "2026-07-21",
    "end_date": "2026-07-21",
    "lower_funnel_caps": {},
    "run_recommendations": false,
    "depvar_configurations": [
      { "name": "US Brand", "deployment_id": 456, "multiplier": 1, "spikes": [] }
    ]
  },
  "results": [
    {
      "depvars": [ { "name": "US Brand", "deployment_id": 456, "weight": 1 } ],
      "expected_outcome": 1280000,
      "total_forecasted_spend": 609524,
      "expected_blended_roi": 2.1,
      "expected_observed_paid_roi": 2.0,
      "expected_roi": 1.9,
      "expected_outcome_quantiles": { "median": 1280000.0, "p25": 1265000.0, "p75": 1295000.0 },
      "expected_blended_roi_quantiles": { "median": 2.1, "p25": 2.05, "p75": 2.15 },
      "expected_observed_paid_roi_quantiles": { "median": 2.0, "p25": 1.9, "p75": 2.1 },
      "expected_roi_quantiles": { "median": 1.9, "p25": 1.8, "p75": 2.0 },
      "downloads": [ { "description": "Budget", "key": "budget" } ]
    }
  ],
  "created_at": "2026-01-15T09:24:00Z",
  "updated_at": "2026-01-15T09:31:00Z"
}

Counterfactuals (altcast_type=actuals or planned): the API returns the underlying forecasts, not the full comparison view shown in the UI's Counterfactual section. It is null for the plan's regular forecast. It is "planned" for a forecast of the plan version's originally specified budget. It is "actuals" for a forecast of the actual budget spent so far.

Both counterfactual entries are ordinary Forecast results with the same shape as any other forecast.

What start_date/end_date mean:

  • Regular forecast (altcast_type: null): start_date/end_date describe only the period that is actually being forecasted going forward. The daily output can extend earlier than start_date, into dates that have already elapsed โ€” those earlier dates are the actual historical outcome, not a forecast.

  • Counterfactual of the actual budget (altcast_type: "actuals"): a fully retroactive forecast. start_date/end_date cover exactly the dates where a complete actual budget is available across every channel included in the forecast, and the entire daily output over that range is counterfactual.

  • Counterfactual of the planned budget (altcast_type: "planned"): start_date/end_date span the retroactive forecast period. Dates after end_date are not in-sample and are a regular forecast of the planned budget. Aggregate metrics like expected_outcome are computed over the entire range of the plan, not just start_dateโ€“end_date.

Note: the forward-looking portion of the planned counterfactual does not match what you get by calling the plan's regular forecast for the same dates, because it takes into account the retroactive forecast for the in-sample time period.

6. Adherence: GET /v1/clients/{client_slug}/plans/{plan_id}/adherence

Adherence compares the spend you planned against the spend that actually happened, channel by channel. It is the API equivalent of the Adherence section of the Plans tab. Use it to check whether a Plan is being followed, to find the channels that have drifted, and to explain why a forecast and an actual outcome disagree.

Adherence is computed per Plan version. A new adherence report is created every time new spend data arrives, so a Plan accumulates reports over time. The index returns all of them, across every version of the Plan. You can filter with plan_version_id and take the most recent report when you want the current picture.

Adherence currently covers spend channels only. Non-spend channels are not included.

If there is no actual spend data available yet for a Plan's period, no adherence reports exist for it and the index returns an empty list. Planned and actual spend are both reported through report_through_date.

Adherence endpoints:

  • GET /plans/{plan_id}/adherence: adherence reports across every version of the plan, paginated. plan_version_id is an optional query parameter to filter to one version.

  • GET /plans/{plan_id}/adherence/{id}: highlights and available downloads for a single adherence report, where id is that report's id.

  • GET /plans/{plan_id}/adherence/{id}/downloads/{key}: downloads one of the report's CSVs.

Here is an example show response.

JSON
{
  "id": 4211,
  "plan_id": "8d777078-044e-4e3d-8d95-8dc14daf8d93",
  "plan_version_id": "4b026308-cce1-42f0-a1ac-2b6b902766ec",
  "plan_version_number": "v3",
  "status": "success",
  "report_through_date": "2026-06-15",
  "highlights": {
    "total": {
      "planned_spend": 1250000,
      "actual_spend": 1100000
    },
    "spend_channels": {
      "upper_funnel": [
        {
          "channel_name": "meta",
          "planned_spend": 500000,
          "actual_spend": 450000
        }
      ],
      "lower_funnel": [
        {
          "channel_name": "branded_search",
          "lf_option": "capped",
          "planned_spend": null,
          "actual_spend": 48000
        }
      ]
    }
  },
  "downloads": [
    { "description": "All channels adherence", "key": "all-channels-adherence" },
    { "description": "Meta adherence", "key": "meta-adherence" }
  ],
  "created_at": "2026-05-01T12:00:00Z",
  "updated_at": "2026-06-10T09:30:00Z"
}

A lower funnel channel shows planned spend only when that spend was provided in the Plan (lf_option: "manual"). When the option is capped or uncapped the Plan doesn't specify a spend figure, so planned_spend is null and only the actual is reported.

Skill file (Beta)

We have a skill file that can help AI coding assistants get up to speed on the Plans API. It is in beta and is best used for writing boilerplate code. Please review any generated code before running it. You can access the skill file by downloading it here. This Skill is available if you install the Claude plugin or can be downloaded directly from Github.

Code Examples

R

# Recast Plans API: R Usage Example
#
# Workflow: List plans โ†’ find primary version โ†’ fetch version detail โ†’ download budget CSV
#
# Prerequisites:
#   install.packages(c("httr2", "jsonlite"))
#   Add to .Renviron: API_PAT=gr_your_token_here
#     Generate your token: log into the Recast app โ†’ click your email (top right) โ†’ Generate API token
#     Store it in ~/.Renviron as: API_PAT=gr_your_token_here
#     Never hard-code or share this token.

library(httr2)
library(jsonlite)

# โ”€โ”€ Edit these โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
CLIENT_SLUG <- "myslug"                    # Your client slug (visible in the app URL)
PLAN_LABEL  <- "Q3 Growth Plan"            # Set to NULL to just use the first plan returned

# โ”€โ”€ Rarely needs changing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
BASE_URL <- "https://api.getrecast.com"
PAT      <- Sys.getenv("API_PAT")             # Set in ~/.Renviron, never hard-coded

# โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
parse <- \(resp) resp |> resp_body_string() |> fromJSON(simplifyVector = FALSE)

check <- function(resp, expected = 200) {
  if (resp_status(resp) != expected) {
    body <- tryCatch(resp_body_string(resp), error = \(e) "(no body)")
    stop(sprintf("HTTP %d: %s", resp_status(resp), body))
  }
  resp
}

api_request <- function(...) {
  request(BASE_URL) |>
    req_url_path_append("v1", "clients", CLIENT_SLUG, ...) |>
    req_auth_bearer_token(PAT) |>
    req_headers(Accept = "application/json") |>
    req_error(is_error = \(resp) FALSE)
}

# โ”€โ”€ Step 1: Find the plan โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
plans <- api_request("plans") |>
  req_url_query(per_page = 100) |>
  req_perform() |> check() |> parse() |>
  (\(r) r$data)()

plan <- if (!is.null(PLAN_LABEL)) {
  Filter(function(p) p$label == PLAN_LABEL, plans)[[1]]
} else {
  plans[[1]]
}
cat(sprintf("Using plan: %s (%s)\n", plan$label, plan$id))

version_id <- plan$primary_version$id  # every plan has a primary version

# โ”€โ”€ Step 2: Fetch the version detail โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
version <- api_request("plans", plan$id, "versions", version_id) |>
  req_perform() |> check() |> parse()
cat(sprintf("Version %s: total_spend=%s, channels=%s\n",
            version$version_number, version$total_spend,
            paste(version$budget$spend_channels, collapse = ", ")))

# โ”€โ”€ Step 3: Download the daily budget CSV โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
csv_resp <- api_request("plans", plan$id, "versions", version_id, "budget") |>
  req_headers(Accept = "text/csv") |>
  req_perform() |> check()

df <- read.csv(text = resp_body_string(csv_resp), check.names = FALSE)
write.csv(df, file = "plan_budget.csv", row.names = FALSE)
cat(sprintf("Saved plan_budget.csv (%d rows)\n", nrow(df)))

Python

# Recast Plans API: Python Usage Example
#
# Workflow: List plans โ†’ find primary version โ†’ fetch version detail โ†’ download budget CSV
#
# Prerequisites:
#   pip install requests pandas
#   Generate your token: log into the Recast app โ†’ click your email (top right) โ†’ Generate API token
#   Set as an environment variable: export API_PAT=gr_your_token_here
#   Never hard-code or share this token.

import os
import io
import requests
import pandas as pd

# โ”€โ”€ Edit these โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
CLIENT_SLUG = "myslug"                     # Your client slug (visible in the app URL)
PLAN_LABEL  = "Q3 Growth Plan"             # Set to None to just use the first plan returned

# โ”€โ”€ Rarely needs changing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
BASE_URL = "https://api.getrecast.com"
PAT      = os.environ["API_PAT"]              # Set via: export API_PAT=gr_your_token_here

BASE_PATH = f"{BASE_URL}/v1/clients/{CLIENT_SLUG}"
HEADERS   = {"Authorization": f"Bearer {PAT}", "Accept": "application/json"}

def check(resp, expected=200):
    if resp.status_code != expected:
        raise Exception(f"HTTP {resp.status_code}: {resp.text or '(no body)'}")
    return resp

# โ”€โ”€ Step 1: Find the plan โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
plans = check(requests.get(f"{BASE_PATH}/plans", headers=HEADERS, params={"per_page": 100})).json()["data"]
plan = next((p for p in plans if p["label"] == PLAN_LABEL), plans[0]) if PLAN_LABEL else plans[0]
print(f"Using plan: {plan['label']} ({plan['id']})")

version_id = plan["primary_version"]["id"]  # every plan has a primary version

# โ”€โ”€ Step 2: Fetch the version detail โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
version = check(requests.get(f"{BASE_PATH}/plans/{plan['id']}/versions/{version_id}", headers=HEADERS)).json()
print(f"Version {version['version_number']}: total_spend={version['total_spend']}, "
      f"channels={', '.join(version['budget']['spend_channels'])}")

# โ”€โ”€ Step 3: Download the daily budget CSV โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
csv_resp = check(requests.get(
    f"{BASE_PATH}/plans/{plan['id']}/versions/{version_id}/budget",
    headers={**HEADERS, "Accept": "text/csv"},
))
df = pd.read_csv(io.StringIO(csv_resp.text))
df.to_csv("plan_budget.csv", index=False)
print(f"Saved plan_budget.csv ({len(df)} rows)")