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 |
|
Plan type |
|
|
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 |
|
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 ( |
Recommended Workflow
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 |
|---|---|---|
|
|
integer |
Default |
|
|
integer |
Default |
|
|
string |
Filter to |
|
|
string |
Partial, case-insensitive match on the plan's label |
|
|
string |
Filter to one or more of |
|
|
string (UUID) |
Filter to plans whose primary version can forecast this KPI |
|
|
string |
Filter to plans created by a specific user (email), or |
Response
{
"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
{
"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
{
"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}/forecastsshows forecasts across every version of the plan. Passplannedoractualsintoaltcast_typesto 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_datedescribe only the period that is actually being forecasted going forward. The daily output can extend earlier thanstart_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_datecover 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_datespan the retroactive forecast period. Dates afterend_dateare not in-sample and are a regular forecast of the planned budget. Aggregate metrics likeexpected_outcomeare computed over the entire range of the plan, not juststart_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_idis an optional query parameter to filter to one version. -
GET /plans/{plan_id}/adherence/{id}: highlights and available downloads for a single adherence report, whereidis 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.
{
"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)")