Facebook reporting
GET/v1/facebook/accounts
List ad accounts
Scope: read:campaignsRole: viewer+
Connected ad accounts with currency, timezone, Meta account status, platform fee, and last sync times. account_status is Meta’s enum name (ACTIVE, DISABLED, UNSETTLED, …), refreshed every few hours. fee_pct is the platform surcharge you set for the account (0 when none).
Request
curl -X GET "https://api.roas.to/v1/facebook/accounts" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"count": 2,
"accounts": [
{
"fb_account_id": "act_123456789",
"name": "My Ad Account",
"account_status": "ACTIVE",
"account_status_code": 1,
"disable_reason": 0,
"currency": "USD",
"timezone_name": "America/New_York",
"business_id": "987654321",
"is_active": true,
"fee_pct": 2,
"last_campaigns_sync": "2026-04-05T09:30:00.000-04:00",
"last_insights_sync": "2026-04-05T09:15:00.000-04:00",
"created_at": "2026-01-12T14:02:11.000-05:00"
}
]
}
PATCH/v1/facebook/accounts/{account_id}
Update an ad account’s settings
Scope: write:budgetsRole: admin+Idempotency-Key
Set the platform surcharge for an ad account. fee_pct is a percent on top of Meta spend (2 = spend + 2%). It feeds spend_with_fees and profit_after_fees on campaign rows; nothing is sent to Meta.
| Field | In | Type | Description |
|---|
account_idrequired | path | string | Facebook ad account id, e.g. act_1234567890. |
fee_pctrequired | body | number | Surcharge percent, e.g. 2 for 2%. |
Request
curl -X PATCH "https://api.roas.to/v1/facebook/accounts/{account_id}" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"fee_pct": 50
}'
Response 200
{
"account_id": "act_123456789",
"fee_pct": 2
}
GET/v1/facebook/campaigns
List campaigns with performance
Scope: read:campaignsRole: viewer+
Campaigns with aggregated Facebook metrics and tracked conversions for the date range.
Metrics: fb_* fields are what Meta reports; our_* are conversions your tracking recorded; true_* (true_roas, true_cpa, true_revenue) use your workspace conversion source. Money is in the ad account currency; *_usd twins are USD-normalized. *_minor fields are integer minor units (cents).
Revenue provenance: revenue_source is tracker (your tracking/postbacks, e.g. Everflow, matched to the campaign by sub1 = campaign id) or fb_pixel (Meta-reported purchases); revenue_synced_at is when that source last delivered data. Fees: spend_with_fees = spend × (1 + account fee_pct/100); profit_after_fees = true_revenue − spend_with_fees. Bids: bid_strategy / bid_amount_minor are the campaign’s (CBO) or its ad sets’ when they all agree; adset_bid_strategies lists every distinct ad set strategy and adset_bid_amount_minor_min/_max the ad set bid range.
| Field | In | Type | Description |
|---|
account_id | query | string | Facebook ad account id, e.g. act_1234567890. |
status | query | string | Filter by status (ACTIVE, PAUSED, …). ACTIVE means currently serving. |
min_spend | query | number | Only campaigns that spent at least this much (account currency) in the date range. |
has_spend | query | "true" | "false" | true: only campaigns with any spend in the date range. |
date_range | query | "today" | "yesterday" | "today_yesterday" | "last_3_days" | "last_3d" | "last_7_days" | "last_7d" | "last_14_days" | "last_14d" | "last_30_days" | "last_30d" | "last_60_days" | "last_60d" | "last_90_days" | "last_90d" | "this_week" | "last_week" | "this_month" | "mtd" | "all_time" | Relative range in the workspace timezone (e.g. today, yesterday, last_7d, last_30d, this_month, all_time). Default last_30d. |
start_date | query | string (date) | YYYY-MM-DD (inclusive). Use with end_date. |
end_date | query | string (date) | YYYY-MM-DD (inclusive). Max range 548 days. Ignored when date_range is set. |
limit | query | integer | Rows per page (1–1000, default 100). |
offset | query | integer | Rows to skip. |
Request
curl -X GET "https://api.roas.to/v1/facebook/campaigns" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"date_range": {
"start_date": "2026-03-29",
"end_date": "2026-04-05"
},
"count": 12,
"campaigns": [
{
"fb_campaign_id": "120212345678",
"name": "TOF - Broad Interests - US",
"objective": "OUTCOME_SALES",
"status": "ACTIVE",
"daily_budget_minor": 50000,
"lifetime_budget_minor": null,
"spend_cap_minor": null,
"total_ad_sets": 3,
"active_ad_sets": 2,
"fb_impressions": 48210,
"fb_clicks": 1893,
"fb_spend": 1247.55,
"fb_ctr": 3.9261,
"fb_cpc": 0.659,
"our_conversions": 31,
"our_revenue": 4185,
"true_revenue": 4185,
"true_roas": 3.3546,
"true_cpa": 40.24,
"bid_strategy": "LOWEST_COST_WITHOUT_CAP",
"bid_amount_minor": null,
"adset_bid_strategies": [
"LOWEST_COST_WITHOUT_CAP"
],
"adset_bid_amount_minor_min": null,
"adset_bid_amount_minor_max": null,
"fee_pct": 2,
"spend_with_fees": 1272.5,
"profit_after_fees": 2912.5,
"revenue_source": "tracker",
"revenue_synced_at": "2026-04-05T09:41:07.000-04:00"
}
]
}
GET/v1/facebook/campaigns/{campaign_id}
Get a campaign
Scope: read:campaignsRole: viewer+
One campaign with its ad sets and ads (creative + per-ad metrics).
Metrics: fb_* fields are what Meta reports; our_* are conversions your tracking recorded; true_* (true_roas, true_cpa, true_revenue) use your workspace conversion source. Money is in the ad account currency; *_usd twins are USD-normalized. *_minor fields are integer minor units (cents).
Revenue provenance: revenue_source is tracker (your tracking/postbacks, e.g. Everflow, matched to the campaign by sub1 = campaign id) or fb_pixel (Meta-reported purchases); revenue_synced_at is when that source last delivered data. Fees: spend_with_fees = spend × (1 + account fee_pct/100); profit_after_fees = true_revenue − spend_with_fees. Bids: bid_strategy / bid_amount_minor are the campaign’s (CBO) or its ad sets’ when they all agree; adset_bid_strategies lists every distinct ad set strategy and adset_bid_amount_minor_min/_max the ad set bid range.
| Field | In | Type | Description |
|---|
campaign_idrequired | path | string | Numeric Facebook id (campaign / ad set / ad). |
date_range | query | "today" | "yesterday" | "today_yesterday" | "last_3_days" | "last_3d" | "last_7_days" | "last_7d" | "last_14_days" | "last_14d" | "last_30_days" | "last_30d" | "last_60_days" | "last_60d" | "last_90_days" | "last_90d" | "this_week" | "last_week" | "this_month" | "mtd" | "all_time" | Relative range in the workspace timezone (e.g. today, yesterday, last_7d, last_30d, this_month, all_time). Default last_30d. |
start_date | query | string (date) | YYYY-MM-DD (inclusive). Use with end_date. |
end_date | query | string (date) | YYYY-MM-DD (inclusive). Max range 548 days. Ignored when date_range is set. |
Request
curl -X GET "https://api.roas.to/v1/facebook/campaigns/{campaign_id}" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"campaign": {
"fb_campaign_id": "120212345678",
"name": "...",
"status": "ACTIVE"
},
"performance": {
"fb_spend": 1247.55,
"our_conversions": 31,
"our_revenue": 4185,
"true_roas": 3.3546
},
"ad_sets": {
"count": 3,
"items": [
{
"fb_ad_set_id": "120298765432",
"name": "25-44 Broad",
"targeting": {
"age_min": 25,
"age_max": 44,
"genders": [
0
]
},
"ads": [
{
"fb_ad_id": "120211112222",
"name": "V1 - Headline A + Image 2",
"creative": {
"headline": "Stop Wasting Ad Spend",
"body_text": "See your true ROAS in real-time...",
"image_url": "https://...",
"link_url": "https://..."
},
"fb_spend": 312.1,
"our_conversions": 8,
"true_roas": 4.12
}
]
}
]
}
}
GET/v1/facebook/adsets
List ad sets with performance
Scope: read:campaignsRole: viewer+
Ad sets with targeting, budget, bid strategy, and metrics.
Metrics: fb_* fields are what Meta reports; our_* are conversions your tracking recorded; true_* (true_roas, true_cpa, true_revenue) use your workspace conversion source. Money is in the ad account currency; *_usd twins are USD-normalized. *_minor fields are integer minor units (cents).
Revenue provenance: revenue_source is tracker (your tracking/postbacks, e.g. Everflow, matched to the campaign by sub1 = campaign id) or fb_pixel (Meta-reported purchases); revenue_synced_at is when that source last delivered data. Fees: spend_with_fees = spend × (1 + account fee_pct/100); profit_after_fees = true_revenue − spend_with_fees. Bids: bid_strategy / bid_amount_minor are the campaign’s (CBO) or its ad sets’ when they all agree; adset_bid_strategies lists every distinct ad set strategy and adset_bid_amount_minor_min/_max the ad set bid range.
| Field | In | Type | Description |
|---|
campaign_id | query | string | Numeric Facebook id (campaign / ad set / ad). |
account_id | query | string | Facebook ad account id, e.g. act_1234567890. |
status | query | string | Filter by status (ACTIVE, PAUSED, …). ACTIVE means currently serving. |
date_range | query | "today" | "yesterday" | "today_yesterday" | "last_3_days" | "last_3d" | "last_7_days" | "last_7d" | "last_14_days" | "last_14d" | "last_30_days" | "last_30d" | "last_60_days" | "last_60d" | "last_90_days" | "last_90d" | "this_week" | "last_week" | "this_month" | "mtd" | "all_time" | Relative range in the workspace timezone (e.g. today, yesterday, last_7d, last_30d, this_month, all_time). Default last_30d. |
start_date | query | string (date) | YYYY-MM-DD (inclusive). Use with end_date. |
end_date | query | string (date) | YYYY-MM-DD (inclusive). Max range 548 days. Ignored when date_range is set. |
limit | query | integer | Rows per page (1–1000, default 100). |
offset | query | integer | Rows to skip. |
Request
curl -X GET "https://api.roas.to/v1/facebook/adsets" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"date_range": {
"start_date": "2026-03-29",
"end_date": "2026-04-05"
},
"count": 8,
"ad_sets": [
{
"fb_ad_set_id": "120298765432",
"fb_campaign_id": "120212345678",
"name": "25-44 Broad",
"status": "ACTIVE",
"effective_status": "ACTIVE",
"daily_budget_minor": 15000,
"lifetime_budget_minor": null,
"bid_strategy": "LOWEST_COST_WITHOUT_CAP",
"optimization_goal": "OFFSITE_CONVERSIONS",
"targeting": {
"age_min": 25,
"age_max": 44
},
"fb_spend": 742.3,
"our_conversions": 18,
"true_roas": 3.81
}
]
}
GET/v1/facebook/adsets/{adset_id}
Get an ad set
Scope: read:campaignsRole: viewer+
One ad set with all of its ads, creative, and per-ad metrics.
Metrics: fb_* fields are what Meta reports; our_* are conversions your tracking recorded; true_* (true_roas, true_cpa, true_revenue) use your workspace conversion source. Money is in the ad account currency; *_usd twins are USD-normalized. *_minor fields are integer minor units (cents).
Revenue provenance: revenue_source is tracker (your tracking/postbacks, e.g. Everflow, matched to the campaign by sub1 = campaign id) or fb_pixel (Meta-reported purchases); revenue_synced_at is when that source last delivered data. Fees: spend_with_fees = spend × (1 + account fee_pct/100); profit_after_fees = true_revenue − spend_with_fees. Bids: bid_strategy / bid_amount_minor are the campaign’s (CBO) or its ad sets’ when they all agree; adset_bid_strategies lists every distinct ad set strategy and adset_bid_amount_minor_min/_max the ad set bid range.
| Field | In | Type | Description |
|---|
adset_idrequired | path | string | Numeric Facebook id (campaign / ad set / ad). |
date_range | query | "today" | "yesterday" | "today_yesterday" | "last_3_days" | "last_3d" | "last_7_days" | "last_7d" | "last_14_days" | "last_14d" | "last_30_days" | "last_30d" | "last_60_days" | "last_60d" | "last_90_days" | "last_90d" | "this_week" | "last_week" | "this_month" | "mtd" | "all_time" | Relative range in the workspace timezone (e.g. today, yesterday, last_7d, last_30d, this_month, all_time). Default last_30d. |
start_date | query | string (date) | YYYY-MM-DD (inclusive). Use with end_date. |
end_date | query | string (date) | YYYY-MM-DD (inclusive). Max range 548 days. Ignored when date_range is set. |
Request
curl -X GET "https://api.roas.to/v1/facebook/adsets/{adset_id}" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"date_range": {
"start_date": "2026-03-29",
"end_date": "2026-04-05"
},
"ad_set": {
"fb_ad_set_id": "120298765432",
"fb_campaign_id": "120212345678",
"name": "25-44 Broad",
"status": "ACTIVE",
"effective_status": "ACTIVE",
"daily_budget_minor": 15000,
"lifetime_budget_minor": null,
"bid_strategy": "LOWEST_COST_WITHOUT_CAP",
"optimization_goal": "OFFSITE_CONVERSIONS",
"targeting": {
"age_min": 25,
"age_max": 44
},
"fb_spend": 742.3,
"our_conversions": 18,
"true_roas": 3.81
},
"ads": [
{
"fb_ad_id": "120211112222",
"fb_campaign_id": "120212345678",
"name": "V1 - Headline A + Image 2",
"status": "ACTIVE",
"effective_status": "ACTIVE",
"creative": {
"headline": "Stop Wasting Ad Spend",
"body_text": "See your true ROAS...",
"call_to_action": "LEARN_MORE",
"image_url": "https://...",
"link_url": "https://youroffer.com/lp1"
},
"fb_impressions": 12400,
"fb_spend": 312.1,
"fb_ctr": 4.12,
"fb_cpc": 0.61,
"our_conversions": 8,
"our_revenue": 1286,
"true_roas": 4.1206,
"true_cpa": 39.01
}
]
}
GET/v1/facebook/ads
List ads with creative + performance
Scope: read:campaignsRole: viewer+
Ads with full creative (headline, body, media, CTA, landing URL) and metrics.
Metrics: fb_* fields are what Meta reports; our_* are conversions your tracking recorded; true_* (true_roas, true_cpa, true_revenue) use your workspace conversion source. Money is in the ad account currency; *_usd twins are USD-normalized. *_minor fields are integer minor units (cents).
Revenue provenance: revenue_source is tracker (your tracking/postbacks, e.g. Everflow, matched to the campaign by sub1 = campaign id) or fb_pixel (Meta-reported purchases); revenue_synced_at is when that source last delivered data. Fees: spend_with_fees = spend × (1 + account fee_pct/100); profit_after_fees = true_revenue − spend_with_fees. Bids: bid_strategy / bid_amount_minor are the campaign’s (CBO) or its ad sets’ when they all agree; adset_bid_strategies lists every distinct ad set strategy and adset_bid_amount_minor_min/_max the ad set bid range.
| Field | In | Type | Description |
|---|
campaign_id | query | string | Numeric Facebook id (campaign / ad set / ad). |
adset_id | query | string | Numeric Facebook id (campaign / ad set / ad). |
account_id | query | string | Facebook ad account id, e.g. act_1234567890. |
status | query | string | Filter by status (ACTIVE, PAUSED, …). ACTIVE means currently serving. |
date_range | query | "today" | "yesterday" | "today_yesterday" | "last_3_days" | "last_3d" | "last_7_days" | "last_7d" | "last_14_days" | "last_14d" | "last_30_days" | "last_30d" | "last_60_days" | "last_60d" | "last_90_days" | "last_90d" | "this_week" | "last_week" | "this_month" | "mtd" | "all_time" | Relative range in the workspace timezone (e.g. today, yesterday, last_7d, last_30d, this_month, all_time). Default last_30d. |
start_date | query | string (date) | YYYY-MM-DD (inclusive). Use with end_date. |
end_date | query | string (date) | YYYY-MM-DD (inclusive). Max range 548 days. Ignored when date_range is set. |
limit | query | integer | Rows per page (1–1000, default 100). |
offset | query | integer | Rows to skip. |
Request
curl -X GET "https://api.roas.to/v1/facebook/ads" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"count": 24,
"ads": [
{
"fb_ad_id": "120211112222",
"fb_campaign_id": "120212345678",
"name": "V1 - Headline A + Image 2",
"status": "ACTIVE",
"effective_status": "ACTIVE",
"creative": {
"headline": "Stop Wasting Ad Spend",
"body_text": "See your true ROAS...",
"call_to_action": "LEARN_MORE",
"image_url": "https://...",
"link_url": "https://youroffer.com/lp1"
},
"fb_impressions": 12400,
"fb_spend": 312.1,
"fb_ctr": 4.12,
"fb_cpc": 0.61,
"our_conversions": 8,
"our_revenue": 1286,
"true_roas": 4.1206,
"true_cpa": 39.01
}
]
}
GET/v1/facebook/ads/{ad_id}
Get an ad
Scope: read:campaignsRole: viewer+
One ad with creative, launch info, and a day-by-day breakdown.
Metrics: fb_* fields are what Meta reports; our_* are conversions your tracking recorded; true_* (true_roas, true_cpa, true_revenue) use your workspace conversion source. Money is in the ad account currency; *_usd twins are USD-normalized. *_minor fields are integer minor units (cents).
Revenue provenance: revenue_source is tracker (your tracking/postbacks, e.g. Everflow, matched to the campaign by sub1 = campaign id) or fb_pixel (Meta-reported purchases); revenue_synced_at is when that source last delivered data. Fees: spend_with_fees = spend × (1 + account fee_pct/100); profit_after_fees = true_revenue − spend_with_fees. Bids: bid_strategy / bid_amount_minor are the campaign’s (CBO) or its ad sets’ when they all agree; adset_bid_strategies lists every distinct ad set strategy and adset_bid_amount_minor_min/_max the ad set bid range.
| Field | In | Type | Description |
|---|
ad_idrequired | path | string | Numeric Facebook id (campaign / ad set / ad). |
date_range | query | "today" | "yesterday" | "today_yesterday" | "last_3_days" | "last_3d" | "last_7_days" | "last_7d" | "last_14_days" | "last_14d" | "last_30_days" | "last_30d" | "last_60_days" | "last_60d" | "last_90_days" | "last_90d" | "this_week" | "last_week" | "this_month" | "mtd" | "all_time" | Relative range in the workspace timezone (e.g. today, yesterday, last_7d, last_30d, this_month, all_time). Default last_30d. |
start_date | query | string (date) | YYYY-MM-DD (inclusive). Use with end_date. |
end_date | query | string (date) | YYYY-MM-DD (inclusive). Max range 548 days. Ignored when date_range is set. |
Request
curl -X GET "https://api.roas.to/v1/facebook/ads/{ad_id}" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"ad": {
"fb_ad_id": "120211112222",
"name": "V1 - Headline A",
"status": "ACTIVE"
},
"creative": {
"headline": "Stop Wasting Ad Spend",
"body_text": "See your true ROAS...",
"image_url": "https://...",
"link_url": "https://..."
},
"batch": {
"headlines": [
"Stop Wasting Ad Spend",
"Know Your True ROAS"
],
"this_ad_headline": "Stop Wasting Ad Spend"
},
"totals": {
"fb_spend": 312.1,
"our_conversions": 8,
"true_roas": 4.12
},
"daily": [
{
"date": "2026-04-04",
"fb_impressions": 3100,
"fb_spend": 78.2,
"our_conversions": 2,
"our_revenue": 340,
"true_roas": 4.3478
}
]
}
GET/v1/facebook/conversions
List ad-attributed conversions
Scope: read:conversionsRole: viewer+
Tracked conversions attributed to a Facebook ad, with revenue and the campaign / ad set / ad that drove them. For every conversion (including unattributed) use GET /v1/conversions.
| Field | In | Type | Description |
|---|
account_id | query | string | Facebook ad account id, e.g. act_1234567890. |
campaign_id | query | string | Numeric Facebook id (campaign / ad set / ad). |
ad_id | query | string | Numeric Facebook id (campaign / ad set / ad). |
date_range | query | "today" | "yesterday" | "today_yesterday" | "last_3_days" | "last_3d" | "last_7_days" | "last_7d" | "last_14_days" | "last_14d" | "last_30_days" | "last_30d" | "last_60_days" | "last_60d" | "last_90_days" | "last_90d" | "this_week" | "last_week" | "this_month" | "mtd" | "all_time" | Relative range in the workspace timezone (e.g. today, yesterday, last_7d, last_30d, this_month, all_time). Default last_30d. |
start_date | query | string (date) | YYYY-MM-DD (inclusive). Use with end_date. |
end_date | query | string (date) | YYYY-MM-DD (inclusive). Max range 548 days. Ignored when date_range is set. |
limit | query | integer | Rows per page (1–1000, default 100). |
offset | query | integer | Rows to skip. |
Request
curl -X GET "https://api.roas.to/v1/facebook/conversions" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"summary": {
"total_conversions": 31,
"total_revenue": 4185,
"inferred_conversions": 3,
"inferred_revenue": 405,
"deterministic_conversions": 28,
"deterministic_revenue": 3780
},
"count": 31,
"conversions": [
{
"id": "conv_abc123",
"timestamp": "2026-04-04T14:22:08.000-04:00",
"revenue": 127.5,
"transaction_id": "EF-987654",
"page_id": "page_123",
"variant_id": "variant_a",
"product_id": null,
"fb_ad_id": "120211112222",
"fb_ad_set_id": "120298765432",
"fb_campaign_id": "120212345678",
"attribution_method": "sub_direct"
}
]
}
GET/v1/facebook/daily
Daily performance totals
Scope: read:analyticsRole: viewer+
Day-by-day spend, Facebook metrics, and tracked revenue for P&L and time series.
Metrics: fb_* fields are what Meta reports; our_* are conversions your tracking recorded; true_* (true_roas, true_cpa, true_revenue) use your workspace conversion source. Money is in the ad account currency; *_usd twins are USD-normalized. *_minor fields are integer minor units (cents).
Revenue provenance: revenue_source is tracker (your tracking/postbacks, e.g. Everflow, matched to the campaign by sub1 = campaign id) or fb_pixel (Meta-reported purchases); revenue_synced_at is when that source last delivered data. Fees: spend_with_fees = spend × (1 + account fee_pct/100); profit_after_fees = true_revenue − spend_with_fees. Bids: bid_strategy / bid_amount_minor are the campaign’s (CBO) or its ad sets’ when they all agree; adset_bid_strategies lists every distinct ad set strategy and adset_bid_amount_minor_min/_max the ad set bid range.
| Field | In | Type | Description |
|---|
account_id | query | string | Facebook ad account id, e.g. act_1234567890. |
campaign_id | query | string | Numeric Facebook id (campaign / ad set / ad). |
date_range | query | "today" | "yesterday" | "today_yesterday" | "last_3_days" | "last_3d" | "last_7_days" | "last_7d" | "last_14_days" | "last_14d" | "last_30_days" | "last_30d" | "last_60_days" | "last_60d" | "last_90_days" | "last_90d" | "this_week" | "last_week" | "this_month" | "mtd" | "all_time" | Relative range in the workspace timezone (e.g. today, yesterday, last_7d, last_30d, this_month, all_time). Default last_30d. |
start_date | query | string (date) | YYYY-MM-DD (inclusive). Use with end_date. |
end_date | query | string (date) | YYYY-MM-DD (inclusive). Max range 548 days. Ignored when date_range is set. |
Request
curl -X GET "https://api.roas.to/v1/facebook/daily" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"totals": {
"fb_impressions": 142800,
"fb_spend": 3842.1,
"our_conversions": 89,
"our_revenue": 12450,
"true_roas": 3.2402,
"profit_loss": 8607.9
},
"days": 7,
"daily": [
{
"date": "2026-04-04",
"fb_impressions": 21200,
"fb_clicks": 832,
"fb_spend": 548.9,
"fb_ctr": 3.9245,
"fb_cpc": 0.6597,
"active_ads": 24,
"our_conversions": 14,
"our_revenue": 1890,
"true_roas": 3.4433,
"profit_loss": 1341.1
}
]
}
GET/v1/facebook/batches
List completed launches (creative detail)
Scope: read:campaignsRole: viewer+
Completed launches with every creative variation and targeting used. For all launches including failed ones (with reasons), use GET /v1/facebook/launches.
| Field | In | Type | Description |
|---|
account_id | query | string | Facebook ad account id, e.g. act_1234567890. |
campaign_id | query | string | Numeric Facebook id (campaign / ad set / ad). |
limit | query | integer | Rows per page (1–1000, default 100). |
offset | query | integer | Rows to skip. |
Request
curl -X GET "https://api.roas.to/v1/facebook/batches" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"count": 5,
"batches": [
{
"id": "batch_xyz",
"fb_campaign_id": "120212345678",
"ads_created": 12,
"headlines": [
"Stop Wasting Ad Spend",
"Know Your True ROAS"
],
"body_texts": [
"See real-time data...",
"Track every dollar..."
],
"image_ids": [
"img_1",
"img_2",
"img_3"
],
"bid_strategy": "LOWEST_COST_WITHOUT_CAP",
"daily_budget_minor": 5000,
"created_at": "2026-04-01T10:00:00.000-04:00"
}
]
}
GET/v1/facebook/automation
Automation rules + recent actions
Scope: read:automationRole: viewer+
Every automation rule with its configuration, plus the most recent actions taken. For full history with filters use GET /v1/automation/history.
| Field | In | Type | Description |
|---|
account_id | query | string | Facebook ad account id, e.g. act_1234567890. |
limit | query | integer | Rows per page (1–1000, default 100). |
Request
curl -X GET "https://api.roas.to/v1/facebook/automation" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"rules": {
"count": 4,
"items": [
{
"name": "Kill low ROAS ads",
"rule_type": "ad",
"conditions": [
{
"metric": "roas",
"operator": "less_than",
"value": 1.5
}
],
"action_type": "pause_ad",
"is_active": true,
"execution_count": 47
}
]
},
"recent_actions": {
"count": 12,
"items": [
{
"rule_id": "rule_abc",
"fb_ad_id": "120211112222",
"fb_adset_id": null,
"fb_campaign_id": "120212345678",
"action_taken": "pause_ad",
"raw_action_taken": "pause_ad",
"is_alert_only": false,
"reason": "ROAS 0.82 < 1.5 threshold",
"money_saved": 42.1,
"executed_at": "2026-04-04T15:30:00.000-04:00"
}
]
}
}
Facebook analytics
GET/v1/facebook/placements
Placement breakdown
Scope: read:analyticsRole: viewer+
Spend, impressions, clicks, and reach by publisher × position (e.g. facebook_feed, instagram_reels) for the date range, across the workspace or one account / campaign.
| Field | In | Type | Description |
|---|
account_id | query | string | Facebook ad account id, e.g. act_1234567890. |
campaign_id | query | string | Numeric Facebook id (campaign / ad set / ad). |
date_range | query | "today" | "yesterday" | "today_yesterday" | "last_3_days" | "last_3d" | "last_7_days" | "last_7d" | "last_14_days" | "last_14d" | "last_30_days" | "last_30d" | "last_60_days" | "last_60d" | "last_90_days" | "last_90d" | "this_week" | "last_week" | "this_month" | "mtd" | "all_time" | Relative range in the workspace timezone (e.g. today, yesterday, last_7d, last_30d, this_month, all_time). Default last_30d. |
start_date | query | string (date) | YYYY-MM-DD (inclusive). Use with end_date. |
end_date | query | string (date) | YYYY-MM-DD (inclusive). Max range 548 days. Ignored when date_range is set. |
Request
curl -X GET "https://api.roas.to/v1/facebook/placements" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"date_range": {
"start_date": "2026-10-12",
"end_date": "2026-10-19"
},
"placements": [
{
"placement": "facebook_feed",
"publisher_platform": "facebook",
"platform_position": "feed",
"spend": 812.4,
"impressions": 40211,
"clicks": 1602,
"reach": 28844,
"ctr": 3.98,
"cpm": 20.2,
"share_of_spend": 0.61
}
],
"currencies": [
"USD"
]
}
GET/v1/facebook/hourly
Hourly performance
Scope: read:analyticsRole: viewer+
Spend, clicks, conversions, revenue, ROAS, and profit by hour of day (0–23, workspace timezone), for dayparting decisions. date picks one exact day; otherwise date_range (today, yesterday, last_3d, last_7d, this_week, last_week) or start_date + end_date.
| Field | In | Type | Description |
|---|
date | query | string (date) | One exact day (YYYY-MM-DD). |
campaign_id | query | string | Numeric Facebook id (campaign / ad set / ad). |
campaign_search | query | string | Campaign name contains. |
date_range | query | "today" | "yesterday" | "last_3d" | "last_7d" | "last_30d" | "this_week" | "last_week" | "this_month" | "all_time" | Relative range in the workspace timezone. Not every endpoint supports every value. |
start_date | query | string (date) | YYYY-MM-DD, use with end_date. |
end_date | query | string (date) | YYYY-MM-DD (inclusive). |
Request
curl -X GET "https://api.roas.to/v1/facebook/hourly" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"data_source": "facebook_ad_insights_hourly",
"currencies": [
"USD"
],
"hours": [
{
"hour": 9,
"hour_label": "9 AM",
"spend": 41.2,
"clicks": 133,
"conversions": 4,
"revenue": 196,
"roas": 4.76,
"cvr": 3.01,
"profit": 154.8
}
],
"summary": {
"total_spend": 812.4,
"total_revenue": 2944,
"total_profit": 2131.6,
"total_conversions": 61,
"total_clicks": 2311,
"overall_roas": 3.62,
"peak_hour": 21,
"worst_hour": 4
}
}
GET/v1/facebook/profitability
Profitability
Scope: read:analyticsRole: viewer+
Spend vs tracked revenue → profit and ROAS, grouped by campaign, day, or hour. start_hour / end_hour restrict to a time window (workspace timezone).
| Field | In | Type | Description |
|---|
group_by | query | "campaign" | "day" | "hour" | |
campaign_search | query | string | |
start_hour | query | integer | |
end_hour | query | integer | |
date_range | query | "today" | "yesterday" | "last_3d" | "last_7d" | "last_30d" | "this_week" | "last_week" | "this_month" | "all_time" | Relative range in the workspace timezone. Not every endpoint supports every value. |
start_date | query | string (date) | YYYY-MM-DD, use with end_date. |
end_date | query | string (date) | YYYY-MM-DD (inclusive). |
Request
curl -X GET "https://api.roas.to/v1/facebook/profitability" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"data_source": "independent_tracking",
"currencies": [
"USD"
],
"breakdown": [
{
"campaign_id": "120210000000000001",
"campaign_name": "TOF — Broad — US",
"spend": 812.4,
"revenue": 2944,
"profit": 2131.6,
"roas": 3.62,
"conversions": 61,
"clicks": 2311
}
],
"summary": {
"total_spend": 812.4,
"total_revenue": 2944,
"total_profit": 2131.6,
"roas": 3.62,
"best_period": null,
"worst_period": null
},
"meta": {
"date_range": "last_7d",
"start_date": "2026-10-12",
"end_date": "2026-10-19",
"group_by": "campaign"
}
}
GET/v1/facebook/funnel
Funnel per campaign
Scope: read:analyticsRole: viewer+
Per campaign: ad clicks → landing-page views → offer clicks → sales, with the step rates and spend, to see whether a campaign leaks at the ad, the lander, or the offer. Landing views and clicks are bot-filtered.
| Field | In | Type | Description |
|---|
account_id | query | string | Facebook ad account id, e.g. act_1234567890. |
campaign_id | query | string | Numeric Facebook id (campaign / ad set / ad). |
status | query | "active" | "all" | |
date_range | query | "today" | "yesterday" | "last_3d" | "last_7d" | "last_14d" | "last_30d" | "this_month" | "all_time" | |
start_date | query | string (date) | |
end_date | query | string (date) | |
limit | query | integer | Rows (1–500, default 100). |
Request
curl -X GET "https://api.roas.to/v1/facebook/funnel" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"campaigns": [
{
"campaign_id": "120210000000000001",
"campaign_name": "TOF — Broad — US",
"fb_account_id": "act_123456789",
"currency": "USD",
"spend": 812.4,
"link_clicks": 2311,
"landing_views": 1980,
"landing_clicks": 655,
"conversions": 61,
"revenue": 2944,
"view_rate": 0.857,
"click_through_rate": 0.331,
"conversion_rate": 0.093
}
]
}
GET/v1/facebook/account-health
Ad account health
Scope: read:campaignsRole: viewer+
Per ad account: Meta account status + disable reason, funding source, sync health, automation health, rate-limit usage, and connected pixels / pages.
| Field | In | Type | Description |
|---|
account_search | query | string | |
include_assets | query | "true" | "false" | |
limit | query | integer | Rows (1–50, default 20). |
Request
curl -X GET "https://api.roas.to/v1/facebook/account-health" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"accounts": [
{
"account_id": "act_123456789",
"name": "Main",
"account_status": "ACTIVE",
"funding_source_present": true,
"currency": "USD",
"sync_tier": "hot"
}
],
"summary": {
"total": 1,
"active": 1,
"disabled": 0
}
}
GET/v1/facebook/data-freshness
Data freshness
Scope: read:analyticsRole: viewer+
How fresh each account’s data is: last insights / structure sync, minutes behind, sync failures, and 24h spend probe. Check this before acting on "today" numbers.
| Field | In | Type | Description |
|---|
account_search | query | string | |
limit | query | integer | Rows (1–50, default 20). |
Request
curl -X GET "https://api.roas.to/v1/facebook/data-freshness" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"accounts": [
{
"account_id": "act_123456789",
"sync_tier": "hot",
"age_minutes": {
"insights": 2,
"campaigns": 21
},
"sync_failures": {
"consecutive": 0
}
}
]
}
GET/v1/facebook/budget-alerts
Budget recommendations
Scope: read:automationRole: viewer+
The budget optimizer’s alerts: scale opportunities, reallocations, degradations, and new winners, with the suggested budget change and opportunity cost.
| Field | In | Type | Description |
|---|
severity | query | "info" | "warning" | "critical" | "all" | |
alert_type | query | "scale_opportunity" | "reallocation" | "degradation" | "new_winner" | "all" | |
include_dismissed | query | "true" | "false" | |
campaign_search | query | string | |
date_range | query | "today" | "yesterday" | "last_3d" | "last_7d" | "last_30d" | "this_week" | "last_week" | "this_month" | "all_time" | Relative range in the workspace timezone. Not every endpoint supports every value. |
start_date | query | string (date) | YYYY-MM-DD, use with end_date. |
end_date | query | string (date) | YYYY-MM-DD (inclusive). |
limit | query | integer | Rows (1–50, default 20). |
Request
curl -X GET "https://api.roas.to/v1/facebook/budget-alerts" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"alerts": [
{
"id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
"alert_type": "scale_opportunity",
"severity": "warning",
"title": "TOF — Broad — US is under-funded",
"recommendation": "Raise budget 30%",
"metrics": {
"current_roi": 2.6,
"suggested_new_budget": 130,
"currency": "USD"
}
}
],
"summary": {
"total": 1,
"critical": 0,
"warning": 1
}
}
GET/v1/facebook/boosts
Boosted-post campaigns
Scope: read:campaignsRole: viewer+
Boosted-post campaigns with spend and Meta-reported results (boosts inherit post URLs, so they use Meta attribution rather than tracker sub-ids).
| Field | In | Type | Description |
|---|
search | query | string | |
status | query | "ACTIVE" | "PAUSED" | "DELETED" | "ARCHIVED" | "all" | |
sort_by | query | "spend" | "revenue" | "roas" | "clicks" | "impressions" | |
date_range | query | "today" | "yesterday" | "last_3d" | "last_7d" | "last_30d" | "this_week" | "last_week" | "this_month" | "all_time" | Relative range in the workspace timezone. Not every endpoint supports every value. |
start_date | query | string (date) | YYYY-MM-DD, use with end_date. |
end_date | query | string (date) | YYYY-MM-DD (inclusive). |
limit | query | integer | Rows (1–50, default 20). |
Request
curl -X GET "https://api.roas.to/v1/facebook/boosts" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"campaigns": [
{
"name": "Boost: Fall launch post",
"status": "ACTIVE",
"spend": 120.5,
"revenue": 410,
"roas": 3.4,
"currency": "USD"
}
],
"summary": {
"total_spend": 120.5,
"total_revenue": 410,
"avg_roas": 3.4
}
}
GET/v1/facebook/creatives
Creative performance
Scope: read:campaignsRole: viewer+
Which creative wins: performance grouped by creative, headline, image, body text, or ad: spend, CTR, conversions, revenue, ROAS.
| Field | In | Type | Description |
|---|
group_by | query | "creative" | "headline" | "image" | "body_text" | "ad" | |
campaign_id | query | string | Numeric Facebook id (campaign / ad set / ad). |
campaign_search | query | string | |
date_range | query | "today" | "yesterday" | "last_3d" | "last_7d" | "last_30d" | "this_week" | "last_week" | "this_month" | "all_time" | Relative range in the workspace timezone. Not every endpoint supports every value. |
start_date | query | string (date) | YYYY-MM-DD, use with end_date. |
end_date | query | string (date) | YYYY-MM-DD (inclusive). |
limit | query | integer | Rows (1–50, default 20). |
Request
curl -X GET "https://api.roas.to/v1/facebook/creatives" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"creatives": [
{
"group": "Doctors hate this one trick",
"spend": 210.4,
"clicks": 811,
"conversions": 22,
"revenue": 1034,
"roas": 4.91,
"ad_count": 3
}
],
"summary": {
"best_performer": "Doctors hate this one trick",
"worst_performer": null
},
"meta": {
"date_range": "last_7d",
"start_date": "2026-10-12",
"end_date": "2026-10-19",
"group_by": "headline"
}
}
Facebook assets
GET/v1/facebook/creatives/search
Search creatives
Scope: read:campaignsRole: viewer+
Find ads by text in their headline, body, or name.
| Field | In | Type | Description |
|---|
qrequired | query | string | |
account_id | query | string | Facebook ad account id, e.g. act_1234567890. |
limit | query | integer | Rows (1–50, default 20). |
Request
curl -X GET "https://api.roas.to/v1/facebook/creatives/search" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"query": "one trick",
"total": 1,
"shown": 1,
"truncated": false,
"creatives": [
{
"fb_creative_id": "120210000000000004",
"title": "Doctors hate this one trick",
"body": "…",
"fb_account_id": "act_123456789"
}
]
}
GET/v1/facebook/ads/{ad_id}/preview
Ad preview
Scope: read:campaignsRole: viewer+
Meta’s rendered preview of an ad (an iframe snippet) for a placement format, e.g. MOBILE_FEED_STANDARD (default), INSTAGRAM_STANDARD, INSTAGRAM_REELS.
| Field | In | Type | Description |
|---|
ad_idrequired | path | string | Numeric Facebook id (campaign / ad set / ad). |
ad_format | query | string | |
Request
curl -X GET "https://api.roas.to/v1/facebook/ads/{ad_id}/preview" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"ad_id": "120210000000000003",
"ad_format": "MOBILE_FEED_STANDARD",
"preview_iframe": "<iframe src=\"https://www.facebook.com/ads/api/preview_iframe.php?…\"></iframe>"
}
GET/v1/facebook/audiences
List audiences
Scope: read:campaignsRole: viewer+
Custom, lookalike, and saved audiences. Use the ids in included_audience_ids / excluded_audience_ids when launching.
| Field | In | Type | Description |
|---|
type | query | "custom" | "saved" | "all" | |
account_id | query | string | Facebook ad account id, e.g. act_1234567890. |
search | query | string | |
include_inactive | query | "true" | "false" | |
limit | query | integer | Rows (1–200, default 100). |
Request
curl -X GET "https://api.roas.to/v1/facebook/audiences" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"audiences": [
{
"fb_audience_id": "23851234567890123",
"name": "Purchasers 180d",
"type": "custom",
"subtype": "WEBSITE",
"approximate_count_lower_bound": 12000,
"fb_account_id": "act_123456789"
}
]
}
GET/v1/facebook/pixels
List pixels
Scope: read:campaignsRole: viewer+
Meta pixels per ad account. Pass the id as fb_pixel_id when launching.
| Field | In | Type | Description |
|---|
account_id | query | string | Facebook ad account id, e.g. act_1234567890. |
search | query | string | |
include_inactive | query | "true" | "false" | |
limit | query | integer | Rows (1–200, default 100). |
Request
curl -X GET "https://api.roas.to/v1/facebook/pixels" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"pixels": [
{
"fb_pixel_id": "1234567890123456",
"name": "Main pixel",
"fb_account_id": "act_123456789",
"is_active": true
}
]
}
GET/v1/facebook/pages
List Facebook Pages
Scope: read:campaignsRole: viewer+
Facebook Pages ads can run under. Pass account_id to also get launch_block_reason for launching from that ad account (null = OK; no_system_user_access / no_user_token_access = Meta will refuse; fix the Page’s access in Business Settings). Pass the id as fb_page_id when launching.
| Field | In | Type | Description |
|---|
account_id | query | string | Facebook ad account id, e.g. act_1234567890. |
search | query | string | |
limit | query | integer | Rows per page (1–1000, default 100). |
offset | query | integer | Rows to skip. |
Request
curl -X GET "https://api.roas.to/v1/facebook/pages" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"total": 1,
"limit": 100,
"offset": 0,
"has_more": false,
"pages": [
{
"fb_page_id": "104455667788990",
"name": "Acme",
"fb_account_id": null,
"category": "Brand",
"is_default": true,
"active_ad_count": 41,
"ad_limit": 250,
"launch_block_reason": null
}
]
}
GET/v1/facebook/targeting/interests
Search targeting interests
Scope: read:campaignsRole: viewer+
Look up Meta interest ids by keyword (live from Meta) for interest_ids when launching.
| Field | In | Type | Description |
|---|
qrequired | query | string | |
account_idrequired | query | string | Ad account whose access is used for the lookup. |
limit | query | integer | Rows (1–50, default 25). |
Request
curl -X GET "https://api.roas.to/v1/facebook/targeting/interests" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"interests": [
{
"id": "6003107902433",
"name": "Keto diet",
"audience_size_lower_bound": 21000000,
"audience_size_upper_bound": 24700000,
"path": [
"Interests",
"Fitness and wellness"
]
}
]
}
Facebook management
POST/v1/facebook/status
Pause or activate campaigns, ad sets, or ads
Scope: write:statusRole: editor+dry_runapproval rulesIdempotency-Key
Sets status to ACTIVE or PAUSED on up to 50 entities in one ad account. Each id is re-read from Meta after the write and returned under verified.
Subject to workspace approval rules (202 + pending_action_id). Boosted-post campaigns are excluded.
| Field | In | Type | Description |
|---|
typerequired | body | "campaign" | "adset" | "ad" | Entity level the ids refer to: 'campaign', 'adset', or 'ad'. |
idsrequired | body | string[] | Up to 50 Facebook ids of the given type. |
statusrequired | body | "ACTIVE" | "PAUSED" | Target status. |
fb_account_id | body | string | Ad account that owns the ids. Optional: inferred from the ids when omitted. |
dry_run | body | boolean | Validate and preview the exact change without applying it. Nothing is sent to Meta. |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/facebook/status" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"type": "campaign",
"ids": [
"120210000000000001"
],
"status": "ACTIVE"
}'
Response 200
{
"status": "completed",
"total": 2,
"succeeded": 2,
"failed": 0,
"results": [
{
"id": "120210000000000001",
"ok": true
},
{
"id": "120210000000000002",
"ok": true
}
]
}
POST/v1/facebook/budgets
Set daily budgets
Scope: write:budgetsRole: editor+dry_runapproval rulesIdempotency-Key
Sets the daily budget on up to 50 campaigns (CBO) or ad sets (ABO) in one ad account. Amounts are in the ad account's currency, MAJOR units (dollars, not cents).
Rejected per id when the budget lives at the other level (wrong_budget_type). Member budget caps apply; increases can be queued by approval rules.
| Field | In | Type | Description |
|---|
typerequired | body | "campaign" | "adset" | 'campaign' (CBO) or 'adset' (ABO). |
updatesrequired | body | object[] | |
fb_account_id | body | string | Ad account that owns the ids. Optional: inferred from the ids when omitted. |
dry_run | body | boolean | Validate and preview the exact change without applying it. Nothing is sent to Meta. |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/facebook/budgets" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"type": "campaign",
"updates": [
{
"id": "120210000000000001",
"daily_budget": 50
}
]
}'
Response 200
{
"status": "completed",
"total": 2,
"succeeded": 2,
"failed": 0,
"results": [
{
"id": "120210000000000001",
"ok": true
},
{
"id": "120210000000000002",
"ok": true
}
]
}
POST/v1/facebook/bids
Set bid / cost caps on ad sets
Scope: write:bidsRole: editor+dry_runapproval rulesIdempotency-Key
Sets bid_amount on up to 50 ad sets in one ad account. Only ad sets whose bid strategy uses a cap (bid cap / cost cap) accept it; others return wrong_strategy. Amount is MAJOR account currency.
| Field | In | Type | Description |
|---|
adset_idsrequired | body | string[] | Up to 50 ad set ids (all in one ad account). |
bid_amountrequired | body | number | Bid amount, MAJOR account currency. |
dry_run | body | boolean | Validate and preview the exact change without applying it. Nothing is sent to Meta. |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/facebook/bids" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"adset_ids": [
"120210000000000001"
],
"bid_amount": 50
}'
Response 200
{
"status": "completed",
"total": 2,
"succeeded": 2,
"failed": 0,
"results": [
{
"id": "120210000000000001",
"ok": true
},
{
"id": "120210000000000002",
"ok": true
}
]
}
POST/v1/facebook/delete
Delete or archive campaigns, ad sets, or ads
Scope: write:deleteRole: editor+dry_runapproval rulesIdempotency-Key
Deletes (default) or archives up to 50 entities. Deleting a campaign removes its ad sets and ads. Requires confirm: true. Entities already gone at Meta count as success.
| Field | In | Type | Description |
|---|
typerequired | body | "campaign" | "adset" | "ad" | Entity level the ids refer to: 'campaign', 'adset', or 'ad'. |
idsrequired | body | string[] | Up to 50 Facebook ids of the given type. |
mode | body | "delete" | "archive" | 'delete' removes permanently; 'archive' keeps it in Meta's archive. |
fb_account_id | body | string | Ad account that owns the ids. Optional: inferred from the ids when omitted. |
confirmrequired | body | true | Must be true. Guards against accidental destructive calls. |
dry_run | body | boolean | Validate and preview the exact change without applying it. Nothing is sent to Meta. |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/facebook/delete" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"type": "campaign",
"ids": [
"120210000000000001"
],
"confirm": true
}'
Response 200
{
"status": "completed",
"total": 2,
"succeeded": 2,
"failed": 0,
"results": [
{
"id": "120210000000000001",
"ok": true
},
{
"id": "120210000000000002",
"ok": true
}
]
}
PATCH/v1/facebook/campaigns/{campaign_id}
Edit a campaign
Scope: write:create or write:status or write:budgetsRole: editor+dry_runapproval rulesIdempotency-Key
Change any of name, status, daily_budget (CBO campaigns) in one call. Each field needs its own scope: name → write:create, status → write:status, daily_budget → write:budgets. Fields are applied independently and reported per field.
| Field | In | Type | Description |
|---|
campaign_idrequired | path | string | Numeric Facebook id (campaign / ad set / ad). |
name | body | string | |
status | body | "ACTIVE" | "PAUSED" | |
daily_budget | body | number | |
dry_run | body | boolean | Validate and preview the exact change without applying it. Nothing is sent to Meta. |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X PATCH "https://api.roas.to/v1/facebook/campaigns/{campaign_id}" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"id": "120210000000000001",
"changes": {
"status": {
"status": "completed",
"value": "PAUSED"
},
"name": {
"status": "completed",
"value": "TOF — Broad — US"
}
},
"verified": {
"status": "PAUSED",
"effective_status": "PAUSED",
"name": "TOF — Broad — US"
}
}
PATCH/v1/facebook/adsets/{adset_id}
Edit an ad set
Scope: write:create or write:status or write:budgets or write:bidsRole: editor+dry_runapproval rulesIdempotency-Key
Change any of name, status, daily_budget (ABO), bid_amount, start_time, end_time. Scopes per field: name/schedule → write:create, status → write:status, daily_budget → write:budgets, bid_amount → write:bids. Pass end_time: null to remove an end date.
| Field | In | Type | Description |
|---|
adset_idrequired | path | string | Numeric Facebook id (campaign / ad set / ad). |
name | body | string | |
status | body | "ACTIVE" | "PAUSED" | |
daily_budget | body | number | |
bid_amount | body | number | |
start_time | body | string (date-time) | |
end_time | body | string (date-time) | null | |
dry_run | body | boolean | Validate and preview the exact change without applying it. Nothing is sent to Meta. |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X PATCH "https://api.roas.to/v1/facebook/adsets/{adset_id}" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"id": "120210000000000001",
"changes": {
"status": {
"status": "completed",
"value": "PAUSED"
},
"name": {
"status": "completed",
"value": "TOF — Broad — US"
}
},
"verified": {
"status": "PAUSED",
"effective_status": "PAUSED",
"name": "TOF — Broad — US"
}
}
PATCH/v1/facebook/ads/{ad_id}
Edit an ad
Scope: write:create or write:statusRole: editor+dry_runapproval rulesIdempotency-Key
Change an ad's name (write:create) and/or status (write:status). Ad creative is immutable at Meta. To change copy or media, duplicate the campaign with new creative.
| Field | In | Type | Description |
|---|
ad_idrequired | path | string | Numeric Facebook id (campaign / ad set / ad). |
name | body | string | |
status | body | "ACTIVE" | "PAUSED" | |
dry_run | body | boolean | Validate and preview the exact change without applying it. Nothing is sent to Meta. |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X PATCH "https://api.roas.to/v1/facebook/ads/{ad_id}" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"id": "120210000000000001",
"changes": {
"status": {
"status": "completed",
"value": "PAUSED"
},
"name": {
"status": "completed",
"value": "TOF — Broad — US"
}
},
"verified": {
"status": "PAUSED",
"effective_status": "PAUSED",
"name": "TOF — Broad — US"
}
}
POST/v1/facebook/campaigns/{campaign_id}/duplicate
Duplicate a campaign (with optional overrides)
Scope: write:cloneRole: editor+dry_runapproval rulesIdempotency-Key
Creates a copy of a campaign launched through ROAS, with every setting, targeting, creative, and tracking carried over. Any field you pass replaces just that part (new copy, new media, new budget, new geo, …). Meta creative is immutable, so this is how you swap creative on a winner. The copy gets fresh sub1/sub2/sub3 tracking for its own campaign / ad set / ad ids automatically (see launch), and the source campaign’s automation rules are attached unless copy_rules: false.
Interest / custom-audience targeting always inherits from the original. Runs synchronously (typically 10–40s) and returns the new campaign.
| Field | In | Type | Description |
|---|
campaign_idrequired | path | string | Numeric Facebook id (campaign / ad set / ad). |
daily_budget | body | number | New daily budget (MAJOR currency). Omit to copy the original. |
page_id | body | string | Run under a different Facebook Page. |
start_immediately | body | boolean | Launch ACTIVE (default: created PAUSED). |
copy_rules | body | boolean | Attach every active automation rule the source campaign is assigned to (default true; false skips). Rules set to "all campaigns" already cover the copy. The response reports rules_copied. |
headlines | body | string[] | |
body_texts | body | string[] | |
descriptions | body | string[] | |
image_ids | body | string (uuid)[] | Replace the media with these media-library ids (see GET /v1/media). |
video_thumbnail_ids | body | string (uuid) | null[] | |
custom_cta | body | string | |
countries | body | string[] | |
age_min | body | integer | |
age_max | body | integer | |
genders | body | "1" | "2" | "all"[] | |
devices | body | "mobile" | "desktop"[] | |
exclude_tablet | body | boolean | |
placements | body | string[] | |
campaign_objective | body | string | |
optimization_goal | body | string | |
bid_strategy | body | string | |
advantage_audience | body | boolean | |
advantage_placements | body | boolean | |
advantage_creative | body | boolean | |
ai_disclosure | body | boolean | |
value_rule_set | body | object | |
value_rule_recipe_id | body | string (uuid) | |
end_date | body | string (date) | Ad set end date (YYYY-MM-DD). |
dry_run | body | boolean | Validate and preview the exact change without applying it. Nothing is sent to Meta. |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/facebook/campaigns/{campaign_id}/duplicate" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 201
{
"status": "completed",
"source_campaign_id": "120210000000000001",
"campaign_id": "120210000000000099",
"launch_id": "b9a7c1d2-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
"ads_created": 6,
"rules_copied": 2
}
Automation rules
GET/v1/automation/rules
List automation rules
Scope: read:automationRole: viewer+
Automation rules (pause / activate / scale budget / bid cap / duplicate / budget schedule / alert) with their full configuration. With account_id, returns rules that apply to that account (including workspace-wide rules).
| Field | In | Type | Description |
|---|
account_id | query | string | Facebook ad account id, e.g. act_1234567890. |
active | query | "true" | "false" | |
Request
curl -X GET "https://api.roas.to/v1/automation/rules" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"total": 1,
"rules": [
{
"id": "0f1e2d3c-4b5a-4968-8776-655443322110",
"fb_account_id": null,
"fb_account_ids": [
"act_123456789"
],
"name": "Pause losers after $50",
"description": null,
"action_type": "pause_campaign",
"action_params": {},
"conditions": [
{
"metric": "spend",
"operator": "greater_than",
"value": 50
},
{
"metric": "tracker_conversions",
"operator": "equals",
"value": 0
}
],
"data_interval": "today",
"apply_to": "campaign",
"campaign_ids": [
"120210000000000001"
],
"schedule": "every_15m",
"cooldown_minutes": 60,
"max_actions_per_day": null,
"execute_mode": "execute_and_alert",
"is_active": true,
"last_executed": "2026-10-19T08:45:00.000-04:00",
"execution_count": 14,
"created_at": "2026-09-02T10:00:00.000-04:00"
}
]
}
POST/v1/automation/rules
Create an automation rule
Scope: write:automationRole: admin+approval rulesIdempotency-Key
Creates a rule. Set apply_to: 'all' for every campaign (narrow with fb_account_ids), or 'campaign' / 'adset' / 'ad' with campaign_ids / adset_ids for specific entities. Conditions: up to 20 {metric, operator, value, useTrackerData?} with operators greater_than, less_than, equals, greater_than_or_equal, less_than_or_equal.
Scale rules take action_params: {budget_change_pct, budget_change_type: 'increase' | 'decrease', max_budget_cap, min_budget_floor, cap_currency}; increases require max_budget_cap. Unlike request bodies elsewhere, money inside action_params (caps, floors, bid caps) is integer MINOR units (cents) with its cap_currency, the same stored shape this API returns. Rules that move money (scale budget, bid caps, duplicate, budget schedules) can be queued by approval rules (202). Requires the admin role.
| Field | In | Type | Description |
|---|
fb_account_id | body | string | null | |
fb_account_ids | body | string[] | null | |
namerequired | body | string | |
description | body | string | null | |
rule_group | body | string | null | |
data_interval | body | "today" | "yesterday" | "today_yesterday" | "this_week" | "last_week" | "this_month" | "last_30_days" | "last_7_days" | "last_1d" | "last_3d" | "last_7d" | "last_14d" | "last_30d" | "last_60d" | "last_90d" | "mtd" | "all_time" | |
exclude_days | body | integer | integer[] | null | |
conditions | body | object[] | null | |
rule_type | body | string | |
action_typerequired | body | "pause_campaign" | "activate_campaign" | "pause_adset" | "activate_adset" | "pause_ad" | "activate_ad" | "send_alert" | "scale_budget" | "scale_campaign_budget" | "duplicate_campaign" | "set_budget_schedule" | "adjust_bid_cap" | "set_bid_cap" | |
action_params | body | object | |
apply_to | body | "all" | "campaign" | "adset" | "ad" | |
campaign_ids | body | string[] | null | |
adset_ids | body | string[] | null | |
schedule | body | "every_5m" | "every_15m" | "every_30m" | "hourly" | "every_2h" | "every_4h" | "every_6h" | "daily" | "daily_12pm_utc" | |
cooldown_minutes | body | 0 | 5 | 15 | 30 | 60 | 120 | 240 | 360 | 720 | 1440 | 2880 | 10080 | |
max_actions_per_day | body | integer | null | |
post_failure_cooldown_minutes | body | 0 | 5 | 15 | 30 | 60 | 120 | 240 | 360 | 720 | 1440 | 2880 | 10080 | null | |
execute_mode | body | "execute_and_alert" | "execute" | "alert_only" | |
execute_when_paused | body | boolean | |
active_hours | body | integer[] | null | |
active_days | body | integer[] | null | |
daypart_timezone | body | string | null | |
daypart_window_start | body | string | null | |
daypart_window_end | body | string | null | |
daypart_window_mode | body | "active" | "pause" | null | |
applies_to_boosts | body | boolean | |
is_active | body | boolean | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/automation/rules" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "name",
"action_type": "pause_campaign"
}'
Response 201
{
"status": "completed",
"rule": {
"id": "0f1e2d3c-4b5a-4968-8776-655443322110",
"fb_account_id": null,
"fb_account_ids": [
"act_123456789"
],
"name": "Pause losers after $50",
"description": null,
"action_type": "pause_campaign",
"action_params": {},
"conditions": [
{
"metric": "spend",
"operator": "greater_than",
"value": 50
},
{
"metric": "tracker_conversions",
"operator": "equals",
"value": 0
}
],
"data_interval": "today",
"apply_to": "campaign",
"campaign_ids": [
"120210000000000001"
],
"schedule": "every_15m",
"cooldown_minutes": 60,
"max_actions_per_day": null,
"execute_mode": "execute_and_alert",
"is_active": true,
"last_executed": "2026-10-19T08:45:00.000-04:00",
"execution_count": 14,
"created_at": "2026-09-02T10:00:00.000-04:00"
}
}
GET/v1/automation/rules/{rule_id}
Get an automation rule
Scope: read:automationRole: viewer+
| Field | In | Type | Description |
|---|
rule_idrequired | path | string (uuid) | |
Request
curl -X GET "https://api.roas.to/v1/automation/rules/{rule_id}" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"id": "0f1e2d3c-4b5a-4968-8776-655443322110",
"fb_account_id": null,
"fb_account_ids": [
"act_123456789"
],
"name": "Pause losers after $50",
"description": null,
"action_type": "pause_campaign",
"action_params": {},
"conditions": [
{
"metric": "spend",
"operator": "greater_than",
"value": 50
},
{
"metric": "tracker_conversions",
"operator": "equals",
"value": 0
}
],
"data_interval": "today",
"apply_to": "campaign",
"campaign_ids": [
"120210000000000001"
],
"schedule": "every_15m",
"cooldown_minutes": 60,
"max_actions_per_day": null,
"execute_mode": "execute_and_alert",
"is_active": true,
"last_executed": "2026-10-19T08:45:00.000-04:00",
"execution_count": 14,
"created_at": "2026-09-02T10:00:00.000-04:00"
}
PATCH/v1/automation/rules/{rule_id}
Update an automation rule
Scope: write:automationRole: admin+approval rulesIdempotency-Key
Pass only the fields to change. Everything else keeps its current value, then the whole rule is re-validated. The account anchor (fb_account_id / fb_account_ids) cannot be changed; create a new rule instead. Edits that leave the rule moving money can be queued by approval rules (202).
| Field | In | Type | Description |
|---|
rule_idrequired | path | string (uuid) | |
name | body | string | |
description | body | string | null | |
rule_group | body | string | null | |
data_interval | body | "today" | "yesterday" | "today_yesterday" | "this_week" | "last_week" | "this_month" | "last_30_days" | "last_7_days" | "last_1d" | "last_3d" | "last_7d" | "last_14d" | "last_30d" | "last_60d" | "last_90d" | "mtd" | "all_time" | |
exclude_days | body | integer | integer[] | null | |
conditions | body | object[] | null | |
rule_type | body | string | |
action_type | body | "pause_campaign" | "activate_campaign" | "pause_adset" | "activate_adset" | "pause_ad" | "activate_ad" | "send_alert" | "scale_budget" | "scale_campaign_budget" | "duplicate_campaign" | "set_budget_schedule" | "adjust_bid_cap" | "set_bid_cap" | |
action_params | body | object | |
apply_to | body | "all" | "campaign" | "adset" | "ad" | |
campaign_ids | body | string[] | null | |
adset_ids | body | string[] | null | |
schedule | body | "every_5m" | "every_15m" | "every_30m" | "hourly" | "every_2h" | "every_4h" | "every_6h" | "daily" | "daily_12pm_utc" | |
cooldown_minutes | body | 0 | 5 | 15 | 30 | 60 | 120 | 240 | 360 | 720 | 1440 | 2880 | 10080 | |
max_actions_per_day | body | integer | null | |
post_failure_cooldown_minutes | body | 0 | 5 | 15 | 30 | 60 | 120 | 240 | 360 | 720 | 1440 | 2880 | 10080 | null | |
execute_mode | body | "execute_and_alert" | "execute" | "alert_only" | |
execute_when_paused | body | boolean | |
active_hours | body | integer[] | null | |
active_days | body | integer[] | null | |
daypart_timezone | body | string | null | |
daypart_window_start | body | string | null | |
daypart_window_end | body | string | null | |
daypart_window_mode | body | "active" | "pause" | null | |
applies_to_boosts | body | boolean | |
is_active | body | boolean | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X PATCH "https://api.roas.to/v1/automation/rules/{rule_id}" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"rule": {
"id": "0f1e2d3c-4b5a-4968-8776-655443322110",
"fb_account_id": null,
"fb_account_ids": [
"act_123456789"
],
"name": "Pause losers after $50",
"description": null,
"action_type": "pause_campaign",
"action_params": {},
"conditions": [
{
"metric": "spend",
"operator": "greater_than",
"value": 50
},
{
"metric": "tracker_conversions",
"operator": "equals",
"value": 0
}
],
"data_interval": "today",
"apply_to": "campaign",
"campaign_ids": [
"120210000000000001"
],
"schedule": "every_15m",
"cooldown_minutes": 60,
"max_actions_per_day": null,
"execute_mode": "execute_and_alert",
"is_active": true,
"last_executed": "2026-10-19T08:45:00.000-04:00",
"execution_count": 14,
"created_at": "2026-09-02T10:00:00.000-04:00"
}
}
DELETE/v1/automation/rules/{rule_id}
Delete an automation rule
Scope: write:automationRole: admin+Idempotency-Key
Deletes the rule. For budget-schedule rules, the Facebook budget schedules it created are removed too; if Meta can't be reached the call fails unless force=true (left-over schedules expire at their end time). Requires ?confirm=true.
| Field | In | Type | Description |
|---|
rule_idrequired | path | string (uuid) | |
confirmrequired | query | "true" | Must be "true". Guards against accidental deletes. |
force | query | "true" | "false" | Delete even if Facebook cleanup fails. |
Request
curl -X DELETE "https://api.roas.to/v1/automation/rules/{rule_id}?confirm=true" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"status": "completed",
"deleted_budget_schedules": 0,
"skipped_budget_schedules": 0
}
POST/v1/automation/rules/{rule_id}/status
Pause or resume an automation rule
Scope: write:automationRole: admin+Idempotency-Key
Turns the rule on (active: true) or off.
| Field | In | Type | Description |
|---|
rule_idrequired | path | string (uuid) | |
activerequired | body | boolean | true = rule runs; false = paused. |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/automation/rules/{rule_id}/status" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"active": true
}'
Response 200
{
"status": "completed",
"rule_id": "0f1e2d3c-4b5a-4968-8776-655443322110",
"active": false
}
POST/v1/automation/rules/{rule_id}/campaigns
Attach a rule to campaigns
Scope: write:automationRole: admin+Idempotency-Key
Adds campaigns to a campaign-scoped rule (for a workspace-wide rule, re-includes previously excluded campaigns). Results are per campaign.
| Field | In | Type | Description |
|---|
rule_idrequired | path | string (uuid) | |
campaign_idsrequired | body | string[] | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/automation/rules/{rule_id}/campaigns" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"campaign_ids": [
"120210000000000001"
]
}'
Response 200
{
"status": "completed",
"rule_id": "0f1e2d3c-4b5a-4968-8776-655443322110",
"results": [
{
"campaign_id": "120210000000000001",
"ok": true,
"already_assigned": false
}
],
"total_campaigns": 2
}
DELETE/v1/automation/rules/{rule_id}/campaigns/{campaign_id}
Detach a rule from a campaign
Scope: write:automationRole: admin+Idempotency-Key
Removes the campaign from a campaign-scoped rule (for a workspace-wide rule, excludes the campaign).
| Field | In | Type | Description |
|---|
rule_idrequired | path | string (uuid) | |
campaign_idrequired | path | string | Numeric Facebook id (campaign / ad set / ad). |
Request
curl -X DELETE "https://api.roas.to/v1/automation/rules/{rule_id}/campaigns/{campaign_id}" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"status": "completed",
"removed": true
}
GET/v1/automation/rules/{rule_id}/executions
List a rule’s executions
Scope: read:automationRole: viewer+
What the rule did, newest first: action, target, old → new value, money saved, success or failure.
| Field | In | Type | Description |
|---|
rule_idrequired | path | string (uuid) | |
limit | query | integer | Rows per page (1–1000, default 100). |
offset | query | integer | Rows to skip. |
Request
curl -X GET "https://api.roas.to/v1/automation/rules/{rule_id}/executions" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"total": 1,
"limit": 100,
"offset": 0,
"has_more": false,
"executions": [
{
"id": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b",
"executed_at": "2026-10-19T08:45:00.000-04:00",
"action": "pause_campaign",
"status": "success",
"reason": "spend 52.10 > 50 and tracker_conversions 0 = 0",
"fb_campaign_id": "120210000000000001",
"fb_adset_id": null,
"fb_ad_id": null,
"old_value": "ACTIVE",
"new_value": "PAUSED",
"money_saved": 31.4
}
]
}
GET/v1/automation/history
Automation history
Scope: read:automationRole: viewer+
Everything your automation did across all rules: pauses, scales, duplicates, and alerts, with targets, values, and money saved.
| Field | In | Type | Description |
|---|
action | query | "paused" | "scaled" | "duplicated" | "alerted" | "all" | |
status | query | "success" | "failure" | "all" | |
campaign_search | query | string | |
date_range | query | "today" | "yesterday" | "last_3d" | "last_7d" | "last_30d" | "this_week" | "last_week" | "this_month" | "all_time" | Relative range in the workspace timezone. Not every endpoint supports every value. |
start_date | query | string (date) | YYYY-MM-DD, use with end_date. |
end_date | query | string (date) | YYYY-MM-DD (inclusive). |
limit | query | integer | Rows (1–50, default 20). |
Request
curl -X GET "https://api.roas.to/v1/automation/history" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"logs": [
{
"id": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b",
"executed_at": "2026-10-19T08:45:00.000-04:00",
"action_taken": "pause_campaign",
"status": "success",
"rule": {
"name": "Pause losers after $50"
},
"target": {
"campaign_id": "120210000000000001"
},
"money_saved": 31.4
}
],
"summary": {
"total_actions": 1,
"success_count": 1,
"failure_count": 0,
"money_saved": 31.4
}
}
Clone rules
GET/v1/clone-rules
List clone rules
Scope: read:automationRole: viewer+
Auto-scaling rules that duplicate winning campaigns when their conditions are met.
| Field | In | Type | Description |
|---|
account_id | query | string | Facebook ad account id, e.g. act_1234567890. |
active | query | "true" | "false" | |
Request
curl -X GET "https://api.roas.to/v1/clone-rules" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"total": 1,
"rules": [
{
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"fb_account_id": "act_123456789",
"name": "Scale winners",
"conditions": [
{
"metric": "tracker_roas",
"operator": "greater_than_or_equal",
"value": 2.5
}
],
"data_interval": "last_3d",
"clone_mode": "simple",
"budget_override": 100,
"start_immediately": true,
"apply_to": "campaign",
"campaign_ids": [
"120210000000000001"
],
"max_clones_per_day": 3,
"min_hours_between_clones": 24,
"is_active": true,
"created_at": "2026-09-02T10:00:00.000-04:00"
}
]
}
POST/v1/clone-rules
Create a clone rule
Scope: write:automationRole: admin+approval rulesIdempotency-Key
Creates an auto-scaling rule (simple mode; AI copy modes are set up in the app). Conditions use spend, tracker_roas, tracker_conversions, tracker_revenue. budget_override is MAJOR currency. Creating a clone rule clears the workspace's campaign-creation approval rules (202 when queued).
| Field | In | Type | Description |
|---|
fb_account_idrequired | body | string | |
namerequired | body | string | |
description | body | string | null | |
conditionsrequired | body | object[] | |
data_interval | body | "today" | "yesterday" | "today_yesterday" | "this_week" | "last_week" | "this_month" | "last_30_days" | "last_7_days" | "last_1d" | "last_3d" | "last_7d" | "last_14d" | "last_30d" | "last_60d" | "last_90d" | "mtd" | "all_time" | |
clone_mode | body | "simple" | "ai_headlines" | "ai_scripts" | "ai_full" | |
budget_override | body | number | null | |
page_override | body | string | null | |
start_immediately | body | boolean | |
copy_automation_rules | body | boolean | |
name_suffix | body | string | |
ai_headline_count | body | integer | null | |
ai_script_count | body | integer | null | |
ai_persona | body | string | null | |
ai_tone | body | string | null | |
ai_provider | body | "anthropic" | "openai" | null | |
ai_model | body | string | null | |
ai_headline_prompt_version | body | string | null | |
ai_script_prompt_version | body | string | null | |
max_clones_per_day | body | integer | |
min_hours_between_clones | body | integer | |
post_failure_cooldown_hours | body | integer | null | |
apply_to | body | "all" | "campaign" | |
campaign_ids | body | string[] | null | |
scheduling_mode | body | "trigger" | "scheduled" | |
scheduled_at | body | string (date-time) | null | |
recurring_pattern | body | "daily" | "weekly" | "monthly" | null | |
recurring_hour | body | integer | null | |
recurring_day | body | integer | null | |
schedule_timezone | body | string | null | |
next_scheduled_run | body | string (date-time) | null | |
active_hours_start | body | integer | null | |
active_hours_end | body | integer | null | |
is_active | body | boolean | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/clone-rules" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"fb_account_id": "act_123456789",
"name": "name",
"conditions": [
{
"metric": "spend",
"operator": "less_than",
"value": 50
}
]
}'
Response 201
{
"status": "completed",
"rule": {
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"fb_account_id": "act_123456789",
"name": "Scale winners",
"conditions": [
{
"metric": "tracker_roas",
"operator": "greater_than_or_equal",
"value": 2.5
}
],
"data_interval": "last_3d",
"clone_mode": "simple",
"budget_override": 100,
"start_immediately": true,
"apply_to": "campaign",
"campaign_ids": [
"120210000000000001"
],
"max_clones_per_day": 3,
"min_hours_between_clones": 24,
"is_active": true,
"created_at": "2026-09-02T10:00:00.000-04:00"
}
}
GET/v1/clone-rules/{rule_id}
Get a clone rule
Scope: read:automationRole: viewer+
| Field | In | Type | Description |
|---|
rule_idrequired | path | string (uuid) | |
Request
curl -X GET "https://api.roas.to/v1/clone-rules/{rule_id}" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"fb_account_id": "act_123456789",
"name": "Scale winners",
"conditions": [
{
"metric": "tracker_roas",
"operator": "greater_than_or_equal",
"value": 2.5
}
],
"data_interval": "last_3d",
"clone_mode": "simple",
"budget_override": 100,
"start_immediately": true,
"apply_to": "campaign",
"campaign_ids": [
"120210000000000001"
],
"max_clones_per_day": 3,
"min_hours_between_clones": 24,
"is_active": true,
"created_at": "2026-09-02T10:00:00.000-04:00"
}
PATCH/v1/clone-rules/{rule_id}
Update a clone rule
Scope: write:automationRole: admin+approval rulesIdempotency-Key
Pass only the fields to change. Material changes (conditions, budget, targets, limits, re-activation) clear the approval rules (202 when queued); name / description / pausing never do.
| Field | In | Type | Description |
|---|
rule_idrequired | path | string (uuid) | |
name | body | string | |
description | body | string | null | |
conditions | body | object[] | |
data_interval | body | "today" | "yesterday" | "today_yesterday" | "this_week" | "last_week" | "this_month" | "last_30_days" | "last_7_days" | "last_1d" | "last_3d" | "last_7d" | "last_14d" | "last_30d" | "last_60d" | "last_90d" | "mtd" | "all_time" | |
clone_mode | body | "simple" | "ai_headlines" | "ai_scripts" | "ai_full" | |
budget_override | body | number | null | |
page_override | body | string | null | |
start_immediately | body | boolean | |
copy_automation_rules | body | boolean | |
name_suffix | body | string | |
ai_headline_count | body | integer | null | |
ai_script_count | body | integer | null | |
ai_persona | body | string | null | |
ai_tone | body | string | null | |
ai_provider | body | "anthropic" | "openai" | null | |
ai_model | body | string | null | |
ai_headline_prompt_version | body | string | null | |
ai_script_prompt_version | body | string | null | |
max_clones_per_day | body | integer | |
min_hours_between_clones | body | integer | |
post_failure_cooldown_hours | body | integer | null | |
apply_to | body | "all" | "campaign" | |
campaign_ids | body | string[] | null | |
scheduling_mode | body | "trigger" | "scheduled" | |
scheduled_at | body | string (date-time) | null | |
recurring_pattern | body | "daily" | "weekly" | "monthly" | null | |
recurring_hour | body | integer | null | |
recurring_day | body | integer | null | |
schedule_timezone | body | string | null | |
next_scheduled_run | body | string (date-time) | null | |
active_hours_start | body | integer | null | |
active_hours_end | body | integer | null | |
is_active | body | boolean | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X PATCH "https://api.roas.to/v1/clone-rules/{rule_id}" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"rule": {
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"fb_account_id": "act_123456789",
"name": "Scale winners",
"conditions": [
{
"metric": "tracker_roas",
"operator": "greater_than_or_equal",
"value": 2.5
}
],
"data_interval": "last_3d",
"clone_mode": "simple",
"budget_override": 100,
"start_immediately": true,
"apply_to": "campaign",
"campaign_ids": [
"120210000000000001"
],
"max_clones_per_day": 3,
"min_hours_between_clones": 24,
"is_active": true,
"created_at": "2026-09-02T10:00:00.000-04:00"
}
}
DELETE/v1/clone-rules/{rule_id}
Delete a clone rule
Scope: write:automationRole: admin+Idempotency-Key
Deletes the rule and its run history. Campaigns it already created are untouched. Requires ?confirm=true.
| Field | In | Type | Description |
|---|
rule_idrequired | path | string (uuid) | |
confirmrequired | query | "true" | Must be "true". Guards against accidental deletes. |
Request
curl -X DELETE "https://api.roas.to/v1/clone-rules/{rule_id}?confirm=true" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"status": "completed"
}
POST/v1/clone-rules/{rule_id}/status
Pause or resume a clone rule
Scope: write:automationRole: admin+approval rulesIdempotency-Key
Pausing is immediate; resuming clears the approval rules (202 when queued).
| Field | In | Type | Description |
|---|
rule_idrequired | path | string (uuid) | |
activerequired | body | boolean | true = rule runs; false = paused. |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/clone-rules/{rule_id}/status" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"active": true
}'
Response 200
{
"status": "completed",
"rule_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"active": false
}
GET/v1/clone-rules/{rule_id}/jobs
List a clone rule’s runs
Scope: read:automationRole: viewer+
Each clone the rule triggered: status, source and new campaign, ads created, errors.
| Field | In | Type | Description |
|---|
rule_idrequired | path | string (uuid) | |
limit | query | integer | Rows per page (1–1000, default 100). |
offset | query | integer | Rows to skip. |
Request
curl -X GET "https://api.roas.to/v1/clone-rules/{rule_id}/jobs" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"total": 1,
"limit": 100,
"offset": 0,
"has_more": false,
"jobs": [
{
"id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"status": "completed",
"parent_campaign_id": "120210000000000001",
"new_campaign_id": "120210000000000099",
"launch_id": "b9a7c1d2-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
"ads_created": 6,
"clone_mode": "simple",
"error_message": null,
"created_at": "2026-10-19T06:00:00.000-04:00",
"completed_at": "2026-10-19T06:00:41.000-04:00"
}
]
}
Auto-boost rules
GET/v1/auto-boost-rules
List auto-boost rules
Scope: read:automationRole: viewer+
Rules that boost organic Page posts once they cross an engagement threshold.
| Field | In | Type | Description |
|---|
account_id | query | string | Facebook ad account id, e.g. act_1234567890. |
Request
curl -X GET "https://api.roas.to/v1/auto-boost-rules" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"total": 1,
"rules": [
{
"id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"fb_account_id": "act_123456789",
"fb_page_id": "104455667788990",
"name": "Boost viral posts",
"is_enabled": true,
"trigger_metric": "engagement_24h_delta",
"trigger_threshold": 200,
"template_adset_fb_id": "120210000000000002",
"daily_budget_major": 25,
"objective": "OUTCOME_ENGAGEMENT",
"cooldown_per_post_hours": 168,
"max_boosts_per_day": 3,
"created_at": "2026-09-02T10:00:00.000-04:00"
}
]
}
POST/v1/auto-boost-rules
Create an auto-boost rule
Scope: write:automationRole: admin+Idempotency-Key
template_adset_fb_id is an existing ad set whose targeting / optimization each boost copies. daily_budget_major is MAJOR currency.
| Field | In | Type | Description |
|---|
namerequired | body | string | |
is_enabled | body | boolean | |
fb_account_idrequired | body | string | |
fb_page_id | body | string | null | |
trigger_metricrequired | body | "likes_24h_delta" | "comments_24h_delta" | "shares_24h_delta" | "engagement_24h_delta" | "engagement_72h_delta" | "total_engagement" | |
trigger_thresholdrequired | body | integer | |
template_adset_fb_idrequired | body | string | |
daily_budget_majorrequired | body | number | |
objective | body | string | |
cooldown_per_post_hours | body | integer | |
max_boosts_per_day | body | integer | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/auto-boost-rules" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "name",
"fb_account_id": "act_123456789",
"trigger_metric": "likes_24h_delta",
"trigger_threshold": 50,
"template_adset_fb_id": "120210000000000001",
"daily_budget_major": 50
}'
Response 201
{
"status": "completed",
"rule": {
"id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"fb_account_id": "act_123456789",
"fb_page_id": "104455667788990",
"name": "Boost viral posts",
"is_enabled": true,
"trigger_metric": "engagement_24h_delta",
"trigger_threshold": 200,
"template_adset_fb_id": "120210000000000002",
"daily_budget_major": 25,
"objective": "OUTCOME_ENGAGEMENT",
"cooldown_per_post_hours": 168,
"max_boosts_per_day": 3,
"created_at": "2026-09-02T10:00:00.000-04:00"
}
}
GET/v1/auto-boost-rules/{rule_id}
Get an auto-boost rule
Scope: read:automationRole: viewer+
| Field | In | Type | Description |
|---|
rule_idrequired | path | string (uuid) | |
Request
curl -X GET "https://api.roas.to/v1/auto-boost-rules/{rule_id}" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"fb_account_id": "act_123456789",
"fb_page_id": "104455667788990",
"name": "Boost viral posts",
"is_enabled": true,
"trigger_metric": "engagement_24h_delta",
"trigger_threshold": 200,
"template_adset_fb_id": "120210000000000002",
"daily_budget_major": 25,
"objective": "OUTCOME_ENGAGEMENT",
"cooldown_per_post_hours": 168,
"max_boosts_per_day": 3,
"created_at": "2026-09-02T10:00:00.000-04:00"
}
PATCH/v1/auto-boost-rules/{rule_id}
Update an auto-boost rule
Scope: write:automationRole: admin+Idempotency-Key
Pass only the fields to change. The ad account cannot be changed.
| Field | In | Type | Description |
|---|
rule_idrequired | path | string (uuid) | |
name | body | string | |
is_enabled | body | boolean | |
fb_page_id | body | string | null | |
trigger_metric | body | "likes_24h_delta" | "comments_24h_delta" | "shares_24h_delta" | "engagement_24h_delta" | "engagement_72h_delta" | "total_engagement" | |
trigger_threshold | body | integer | |
template_adset_fb_id | body | string | |
daily_budget_major | body | number | |
objective | body | string | |
cooldown_per_post_hours | body | integer | |
max_boosts_per_day | body | integer | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X PATCH "https://api.roas.to/v1/auto-boost-rules/{rule_id}" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"rule": {
"id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"fb_account_id": "act_123456789",
"fb_page_id": "104455667788990",
"name": "Boost viral posts",
"is_enabled": true,
"trigger_metric": "engagement_24h_delta",
"trigger_threshold": 200,
"template_adset_fb_id": "120210000000000002",
"daily_budget_major": 25,
"objective": "OUTCOME_ENGAGEMENT",
"cooldown_per_post_hours": 168,
"max_boosts_per_day": 3,
"created_at": "2026-09-02T10:00:00.000-04:00"
}
}
DELETE/v1/auto-boost-rules/{rule_id}
Delete an auto-boost rule
Scope: write:automationRole: admin+Idempotency-Key
Deletes the rule. Boosts it already created keep running. Requires ?confirm=true.
| Field | In | Type | Description |
|---|
rule_idrequired | path | string (uuid) | |
confirmrequired | query | "true" | Must be "true". Guards against accidental deletes. |
Request
curl -X DELETE "https://api.roas.to/v1/auto-boost-rules/{rule_id}?confirm=true" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"status": "completed"
}
POST/v1/auto-boost-rules/{rule_id}/status
Enable or disable an auto-boost rule
Scope: write:automationRole: admin+Idempotency-Key
Turns the rule on or off. Re-enabling clears its last error.
| Field | In | Type | Description |
|---|
rule_idrequired | path | string (uuid) | |
activerequired | body | boolean | true = rule runs; false = paused. |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/auto-boost-rules/{rule_id}/status" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"active": true
}'
Response 200
{
"status": "completed",
"rule_id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"active": true
}
GET/v1/auto-boost-rules/{rule_id}/executions
List an auto-boost rule’s boosts
Scope: read:automationRole: viewer+
Each post the rule boosted (or tried to): status, campaign created, metric at fire time, errors.
| Field | In | Type | Description |
|---|
rule_idrequired | path | string (uuid) | |
limit | query | integer | Rows per page (1–1000, default 100). |
offset | query | integer | Rows to skip. |
Request
curl -X GET "https://api.roas.to/v1/auto-boost-rules/{rule_id}/executions" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"total": 1,
"limit": 100,
"offset": 0,
"has_more": false,
"executions": [
{
"id": "4d5e6f7a-8b9c-4d0e-9f1a-2b3c4d5e6f7a",
"status": "success",
"fb_campaign_id": "120210000000000120",
"metric_value_at_fire": 214,
"error_message": null,
"executed_at": "2026-10-19T07:10:00.000-04:00"
}
]
}
Webhooks
GET/v1/webhooks/event-types
List webhook event types
Scope: read:webhooksRole: viewer+
Every event you can subscribe to, with what triggers it.
Request
curl -X GET "https://api.roas.to/v1/webhooks/event-types" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"event_types": [
{
"type": "test.verdict.winner",
"description": "A page-test variant reached a winning verdict."
}
]
}
GET/v1/webhooks/endpoints
List webhook endpoints
Scope: read:webhooksRole: admin+
Configured endpoints (secrets are never returned).
Deliveries are signed with the Standard Webhooks scheme (webhook-id, webhook-timestamp, webhook-signature headers; HMAC-SHA256 with your whsec_… secret). Retries back off up to 6 times; an endpoint is disabled after 20 consecutive failures.
Request
curl -X GET "https://api.roas.to/v1/webhooks/endpoints" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"endpoints": [
{
"id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d",
"url": "https://example.com/hooks/roas",
"events": [
"*"
],
"description": "Prod receiver",
"is_active": true,
"consecutive_failures": 0,
"disabled_reason": null,
"created_at": "2026-10-01T10:00:00.000-04:00"
}
]
}
POST/v1/webhooks/endpoints
Create a webhook endpoint
Scope: write:webhooksRole: admin+Idempotency-Key
Registers an https URL. The response includes the signing secret. It is shown only once, so store it. Up to 5 endpoints per workspace.
Deliveries are signed with the Standard Webhooks scheme (webhook-id, webhook-timestamp, webhook-signature headers; HMAC-SHA256 with your whsec_… secret). Retries back off up to 6 times; an endpoint is disabled after 20 consecutive failures.
| Field | In | Type | Description |
|---|
urlrequired | body | string (uri) | |
eventsrequired | body | "*" | "test.variant.promoted" | "test.variant.paused" | "test.verdict.winner" | "test.verdict.futility" | "test.variant.created" | "snippet.went_dark" | "launch.completed" | "launch.failed" | "clone.completed" | "clone.failed" | "automation.action_executed" | "conversion.recorded" | "account.access_lost" | "budget_alert.created"[] | Event types to receive, or ['*'] for all. |
description | body | string | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/webhooks/endpoints" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"url": "https://example.com/offer",
"events": [
"*"
]
}'
Response 201
{
"id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d",
"url": "https://example.com/hooks/roas",
"events": [
"*"
],
"description": "Prod receiver",
"is_active": true,
"consecutive_failures": 0,
"disabled_reason": null,
"created_at": "2026-10-01T10:00:00.000-04:00",
"secret": "whsec_Zm9vYmFyYmF6cXV4Li4u"
}
PATCH/v1/webhooks/endpoints/{endpoint_id}
Update a webhook endpoint
Scope: write:webhooksRole: admin+Idempotency-Key
Enable / disable, change subscribed events, or edit the description. Re-enabling resets the failure count.
| Field | In | Type | Description |
|---|
endpoint_idrequired | path | string (uuid) | |
is_active | body | boolean | |
events | body | "*" | "test.variant.promoted" | "test.variant.paused" | "test.verdict.winner" | "test.verdict.futility" | "test.variant.created" | "snippet.went_dark" | "launch.completed" | "launch.failed" | "clone.completed" | "clone.failed" | "automation.action_executed" | "conversion.recorded" | "account.access_lost" | "budget_alert.created"[] | Event types to receive, or ['*'] for all. |
description | body | string | null | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X PATCH "https://api.roas.to/v1/webhooks/endpoints/{endpoint_id}" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed"
}
DELETE/v1/webhooks/endpoints/{endpoint_id}
Delete a webhook endpoint
Scope: write:webhooksRole: admin+Idempotency-Key
Stops deliveries to the endpoint. Requires ?confirm=true.
| Field | In | Type | Description |
|---|
endpoint_idrequired | path | string (uuid) | |
confirmrequired | query | "true" | Must be "true". Guards against accidental deletes. |
Request
curl -X DELETE "https://api.roas.to/v1/webhooks/endpoints/{endpoint_id}" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"status": "completed"
}
POST/v1/webhooks/endpoints/{endpoint_id}/rotate-secret
Rotate a signing secret
Scope: write:webhooksRole: admin+Idempotency-Key
Issues a new signing secret (returned once). The old secret stops verifying immediately.
| Field | In | Type | Description |
|---|
endpoint_idrequired | path | string (uuid) | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/webhooks/endpoints/{endpoint_id}/rotate-secret" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"secret": "whsec_bmV3c2VjcmV0Li4u"
}
POST/v1/webhooks/endpoints/{endpoint_id}/test
Send a test event
Scope: write:webhooksRole: admin+Idempotency-Key
Sends a real, signed webhook.test event through the delivery pipeline to this endpoint only. If your receiver verifies this, it verifies production events.
| Field | In | Type | Description |
|---|
endpoint_idrequired | path | string (uuid) | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/webhooks/endpoints/{endpoint_id}/test" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "queued",
"event_id": "evt_3f1e2d3c-4b5a-4968-8776-655443322110"
}
GET/v1/webhooks/deliveries
List webhook deliveries
Scope: read:webhooksRole: admin+
Delivery attempts, newest first, with status and the receiver’s last HTTP status.
| Field | In | Type | Description |
|---|
endpoint_id | query | string (uuid) | |
status | query | "pending" | "delivered" | "failed" | |
event_type | query | string | |
limit | query | integer | Rows per page (1–1000, default 100). |
offset | query | integer | Rows to skip. |
Request
curl -X GET "https://api.roas.to/v1/webhooks/deliveries" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"total": 1,
"limit": 100,
"offset": 0,
"has_more": false,
"deliveries": [
{
"id": "8b9c0d1e-2f3a-4b4c-9d5e-6f7a8b9c0d1e",
"endpoint_id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d",
"event_id": "evt_3f1e2d3c-4b5a-4968-8776-655443322110",
"event_type": "test.verdict.winner",
"status": "delivered",
"attempts": 1,
"last_response_status": 200,
"last_attempt_at": "2026-10-19T09:00:02.000-04:00",
"created_at": "2026-10-19T09:00:00.000-04:00"
}
]
}
Page tests
GET/v1/tests
List page tests
Scope: read:snippetsRole: viewer+
Every page test in the workspace, newest first, with install health.
| Field | In | Type | Description |
|---|
limit | query | integer | Rows per page (1–100). |
offset | query | integer | Rows to skip. |
Request
curl -X GET "https://api.roas.to/v1/tests" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"tests": [
{
"page_id": "keto-advertorial",
"name": "Keto advertorial",
"url": "https://example.com/keto",
"variant_count": 3,
"active_variant_count": 3,
"status": "active",
"split_mode": "auto",
"auto_generation_enabled": true,
"target_test_count": 2,
"baseline_traffic_pct": 60,
"health": {
"install_state": "installed",
"checked_at": "2026-10-19T08:00:00.000-04:00",
"last_seen_traffic_at": "2026-10-19T08:59:00.000-04:00"
},
"created_at": "2026-09-01T10:00:00.000-04:00"
}
],
"pagination": {
"limit": 20,
"offset": 0,
"returned": 1,
"total": 1,
"has_more": false
}
}
POST/v1/tests
Create a page test
Scope: write:snippetsRole: editor+Idempotency-Key
Creates a test on your page with its baseline (the page as-is, plus any modifications). Add challengers with POST /v1/tests/{page_id}/variants, install the snippet from GET /v1/tests/{page_id}/install.
tracking.mode:
redirect (default): links matched by tracking.linkTargeting are tracked; redirectAction: 'append' keeps each link's own destination, 'replace' sends them to direct_affiliate_link.passthrough: links are untouched; conversions attribute through your on-site pixel.
The page_id is normalized (lowercase, a–z 0–9 -) and suffixed if another workspace already uses it. Always use the returned page_id.
| Field | In | Type | Description |
|---|
page_idrequired | body | string | |
page_namerequired | body | string | |
original_urlrequired | body | string (uri) | |
product_id | body | string | null | |
traffic_source_id | body | string | null | |
direct_affiliate_link | body | string (uri) | null | |
modificationsrequired | body | object[] | |
trackingrequired | body | object | |
detected_elements | body | object | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/tests" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"page_id": "120210000000000001",
"page_name": "page name",
"original_url": "https://example.com/offer",
"modifications": [
{
"selector": "selector",
"action": "replace_text"
}
],
"tracking": {
"linkTargeting": {
"matchers": [
{
"type": "href_contains",
"value": "value"
}
]
}
}
}'
Response 201
{
"status": "completed",
"page_id": "keto-advertorial",
"public_id": "p_8Hc2kQ",
"snippet_url": "https://t.example.com/l/p_8Hc2kQ"
}
GET/v1/tests/{page_id}
Get a page test
Scope: read:snippetsRole: viewer+
One test with all of its variants (including paused and archived).
| Field | In | Type | Description |
|---|
page_idrequired | path | string | Page test id (the page_id slug). |
Request
curl -X GET "https://api.roas.to/v1/tests/{page_id}" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"page_id": "keto-advertorial",
"name": "Keto advertorial",
"url": "https://example.com/keto",
"variant_count": 3,
"active_variant_count": 3,
"status": "active",
"split_mode": "auto",
"auto_generation_enabled": true,
"target_test_count": 2,
"baseline_traffic_pct": 60,
"health": {
"install_state": "installed",
"checked_at": "2026-10-19T08:00:00.000-04:00",
"last_seen_traffic_at": "2026-10-19T08:59:00.000-04:00"
},
"created_at": "2026-09-01T10:00:00.000-04:00",
"variants": [
{
"id": "2f1d7c8e-0a4b-4e5f-9a1b-3c4d5e6f7a8b",
"variant_id": "v2",
"is_baseline": false,
"status": "active",
"stage": "growing",
"traffic_pct": 20,
"hypothesis": "Benefit-led headline beats feature-led",
"generation_method": "ai",
"changes_description": "Headline rewritten around the main benefit",
"test_started_at": "2026-10-12T09:00:00.000-04:00",
"promoted_at": null,
"created_at": "2026-10-12T08:55:00.000-04:00"
}
]
}
PATCH/v1/tests/{page_id}
Edit the baseline
Scope: write:snippetsRole: editor+Idempotency-Key
Replace the baseline's modifications and/or notes. Deployed to the edge immediately.
| Field | In | Type | Description |
|---|
page_idrequired | path | string | Page test id (the page_id slug). |
modifications | body | object[] | |
notes | body | string | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X PATCH "https://api.roas.to/v1/tests/{page_id}" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"kv": "deployed"
}
DELETE/v1/tests/{page_id}
Delete a test permanently
Scope: write:snippetsRole: admin+Idempotency-Key
Deletes the test, its variants, and its recorded views, clicks, and conversions. Cannot be undone; prefer archive. Requires ?confirm=true and the admin role.
| Field | In | Type | Description |
|---|
page_idrequired | path | string | Page test id (the page_id slug). |
confirmrequired | query | "true" | Must be "true". Guards against accidental deletes. |
Request
curl -X DELETE "https://api.roas.to/v1/tests/{page_id}?confirm=true" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"status": "completed",
"kv": "cleaned"
}
GET/v1/tests/{page_id}/results
Get test results + verdict
Scope: read:snippetsRole: viewer+
Per-variant lifetime and evaluation-window stats (views, clicks, conversions, revenue, revenue per view) plus the full Bayesian verdict the optimizer acts on: probability each variant beats the baseline on revenue per view, the practical-equivalence (futility) mass, a 90% credible interval on lift, a winner / loser / equivalent / collecting classification, and a distance-to-decision forecast.
| Field | In | Type | Description |
|---|
page_idrequired | path | string | Page test id (the page_id slug). |
Request
curl -X GET "https://api.roas.to/v1/tests/{page_id}/results" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"page_id": "keto-advertorial",
"eval_started_at": "2026-10-12T09:00:00.000-04:00",
"verdict": {
"status": "collecting",
"leader_variant_id": "v2",
"confidence": 0.83,
"forecast": {
"direction": "winner",
"additional_views": 2400,
"additional_days": 3.1
}
},
"variants": [
{
"id": "2f1d7c8e-0a4b-4e5f-9a1b-3c4d5e6f7a8b",
"variant_id": "v2",
"is_baseline": false,
"status": "active",
"stage": "growing",
"traffic_pct": 20,
"hypothesis": "Benefit-led headline beats feature-led",
"generation_method": "ai",
"changes_description": "Headline rewritten around the main benefit",
"test_started_at": "2026-10-12T09:00:00.000-04:00",
"promoted_at": null,
"created_at": "2026-10-12T08:55:00.000-04:00",
"lifetime": {
"views": 5120,
"clicks": 1490,
"conversions": 61,
"revenue_usd": 2745,
"epv_usd": 0.5361
},
"evaluation": {
"views": 4012,
"clicks": 1170,
"conversions": 49,
"revenue_usd": 2205,
"epv_usd": 0.5496
},
"confidence": 0.83,
"classification": "collecting",
"comparison": {
"p_beat_baseline": 0.83,
"p_within_margin": 0.11,
"p_practical_win": 0.71,
"lift_median": 0.14,
"lift_q05": -0.04,
"lift_q95": 0.33
}
}
]
}
GET/v1/tests/{page_id}/settings
Get test automation settings
Scope: read:snippetsRole: viewer+
Traffic split mode, baseline share, auto-generation, auto-promote / auto-pause / auto-cull, decision thresholds, and holdout. Unset thresholds (null) use the workspace defaults.
| Field | In | Type | Description |
|---|
page_idrequired | path | string | Page test id (the page_id slug). |
Request
curl -X GET "https://api.roas.to/v1/tests/{page_id}/settings" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"page_id": "keto-advertorial",
"split_mode": "auto",
"baseline_traffic_pct": 60,
"target_test_count": 2,
"auto_generation_enabled": true,
"auto_promote_enabled": true,
"auto_pause_enabled": true,
"auto_cull_enabled": false,
"min_views_for_promotion": null,
"min_views_for_eval": null,
"weak_loser_views": null,
"weak_loser_threshold": null,
"promotion_confidence": null,
"pause_threshold": null,
"holdout_enabled": false,
"holdout_percentage": 500
}
PATCH/v1/tests/{page_id}/settings
Edit test automation settings
Scope: write:snippetsRole: editor+Idempotency-Key
Pass only the fields to change; null resets a threshold to the workspace default.
split_mode: 'manual' stops the optimizer from moving traffic, promoting, or pausing on this page.auto_generation_enabled + target_test_count: keep N AI challengers running.- Thresholds:
promotion_confidence (0.85–1), pause_threshold (0.1–0.85), weak_loser_threshold (0.1–0.6, ≤ pause_threshold), view minimums (50–100000).
| Field | In | Type | Description |
|---|
page_idrequired | path | string | Page test id (the page_id slug). |
split_mode | body | "auto" | "manual" | |
baseline_traffic_pct | body | number | |
target_test_count | body | integer | |
auto_generation_enabled | body | boolean | |
auto_promote_enabled | body | boolean | |
auto_pause_enabled | body | boolean | |
auto_cull_enabled | body | boolean | |
holdout_enabled | body | boolean | |
holdout_percentage | body | integer | Holdout share in basis points (500 = 5%). |
min_views_for_promotion | body | integer | null | |
min_views_for_eval | body | integer | null | |
weak_loser_views | body | integer | null | |
weak_loser_threshold | body | number | null | |
promotion_confidence | body | number | null | |
pause_threshold | body | number | null | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X PATCH "https://api.roas.to/v1/tests/{page_id}/settings" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"kv": "deployed"
}
PATCH/v1/tests/{page_id}/tracking
Edit link tracking
Scope: write:snippetsRole: editor+Idempotency-Key
Change how the test's links are tracked, for every active variant: tracking mode, link targeting rules, the replace-destination URL, and stamp timing (click = links stay static until a real click; render = stamped on page load).
| Field | In | Type | Description |
|---|
page_idrequired | path | string | Page test id (the page_id slug). |
mode | body | "redirect" | "passthrough" | |
link_targeting | body | object | |
direct_affiliate_link | body | string (uri) | null | Replace-destination URL. Omit to keep the current one; null clears it. |
stamp_timing | body | "render" | "click" | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X PATCH "https://api.roas.to/v1/tests/{page_id}/tracking" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"kv": "deployed",
"updated": 3
}
GET/v1/tests/{page_id}/install
Get the install snippet
Scope: read:snippetsRole: viewer+
The <script> tag to paste into the page's <head>. It's served from your custom tracking domain (first-party).
| Field | In | Type | Description |
|---|
page_idrequired | path | string | Page test id (the page_id slug). |
Request
curl -X GET "https://api.roas.to/v1/tests/{page_id}/install" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"page_id": "keto-advertorial",
"public_id": "p_8Hc2kQ",
"snippet_url": "https://t.example.com/l/p_8Hc2kQ",
"html": "<script>…</script>"
}
POST/v1/tests/{page_id}/archive
Archive a test
Scope: write:snippetsRole: editor+Idempotency-Key
Stops the test and removes it from the edge (the page renders untouched). Data is kept.
| Field | In | Type | Description |
|---|
page_idrequired | path | string | Page test id (the page_id slug). |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/tests/{page_id}/archive" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"kv": "cleaned"
}
POST/v1/tests/{page_id}/unarchive
Restore an archived test
Scope: write:snippetsRole: editor+Idempotency-Key
Restores the latest baseline at 100%; other variants come back paused.
| Field | In | Type | Description |
|---|
page_idrequired | path | string | Page test id (the page_id slug). |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/tests/{page_id}/unarchive" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"kv": "deployed",
"restored": "baseline",
"variants": 3
}
PUT/v1/tests/{page_id}/split
Set the traffic split
Scope: write:snippetsRole: editor+Idempotency-Key
mode: 'manual' + allocations: pin exact shares for the listed variants (the rest share the remainder). The optimizer stops moving traffic.mode: 'even': split evenly across active variants, manual mode.mode: 'auto': hand traffic back to the optimizer.
| Field | In | Type | Description |
|---|
page_idrequired | path | string | Page test id (the page_id slug). |
moderequired | body | "auto" | "manual" | "even" | |
allocations | body | object[] | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X PUT "https://api.roas.to/v1/tests/{page_id}/split" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"mode": "auto"
}'
Response 200
{
"status": "completed",
"kv": "deployed",
"split_mode": "manual",
"split": {
"baseline": 50,
"v2": 30,
"v3": 20
}
}
POST/v1/tests/{page_id}/evaluation/restart
Restart the evaluation window
Scope: write:snippetsRole: editor+Idempotency-Key
Starts a fresh evaluation window for every active variant (e.g. after you changed the offer). Lifetime stats are kept; the verdict only counts data from now on.
| Field | In | Type | Description |
|---|
page_idrequired | path | string | Page test id (the page_id slug). |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/tests/{page_id}/evaluation/restart" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"variants": [
"baseline",
"v2"
],
"started_at": "2026-10-19T09:00:00.000-04:00"
}
POST/v1/tests/{page_id}/deploy
Force a redeploy to the edge
Scope: write:snippetsRole: editor+Idempotency-Key
Re-publishes the current test config to the edge. Normally automatic.
| Field | In | Type | Description |
|---|
page_idrequired | path | string | Page test id (the page_id slug). |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/tests/{page_id}/deploy" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"kv": "deployed"
}
GET/v1/tests/{page_id}/selector-health
Get selector health
Scope: read:snippetsRole: viewer+
Selector-drift telemetry reported by the snippet: which CSS selectors stopped matching on the live page (e.g. after a site redesign) and how often they recovered.
| Field | In | Type | Description |
|---|
page_idrequired | path | string | Page test id (the page_id slug). |
Request
curl -X GET "https://api.roas.to/v1/tests/{page_id}/selector-health" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"page_id": "keto-advertorial",
"selectors": [
{
"variant_id": "v2",
"selector": "h1.headline",
"last_status": "ok",
"recovery_count": 1,
"failure_count": 3,
"first_seen_at": "2026-10-12T09:00:00.000-04:00",
"last_seen_at": "2026-10-19T08:59:00.000-04:00"
}
]
}
POST/v1/tests/{page_id}/variants
Add a variant
Scope: write:snippetsRole: editor+Idempotency-Key
Adds a challenger. It starts active as a seedling with a small traffic share and the evaluation clock starts automatically. Omit modifications to start from the baseline's. variant_id defaults to the next vN.
| Field | In | Type | Description |
|---|
page_idrequired | path | string | Page test id (the page_id slug). |
variant_id | body | string | |
modifications | body | object[] | |
changes_description | body | string | |
hypothesis | body | string | |
notes | body | string | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/tests/{page_id}/variants" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 201
{
"status": "completed",
"kv": "deployed",
"variant_id": "v3"
}
GET/v1/tests/{page_id}/variants/{variant_id}
Get a variant
Scope: read:snippetsRole: viewer+
One variant of a page test.
| Field | In | Type | Description |
|---|
page_idrequired | path | string | Page test id (the page_id slug). |
variant_idrequired | path | string | Variant id within a test. |
Request
curl -X GET "https://api.roas.to/v1/tests/{page_id}/variants/{variant_id}" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"id": "2f1d7c8e-0a4b-4e5f-9a1b-3c4d5e6f7a8b",
"variant_id": "v2",
"is_baseline": false,
"status": "active",
"stage": "growing",
"traffic_pct": 20,
"hypothesis": "Benefit-led headline beats feature-led",
"generation_method": "ai",
"changes_description": "Headline rewritten around the main benefit",
"test_started_at": "2026-10-12T09:00:00.000-04:00",
"promoted_at": null,
"created_at": "2026-10-12T08:55:00.000-04:00"
}
PATCH/v1/tests/{page_id}/variants/{variant_id}
Edit, pause, or resume a variant
Scope: write:snippetsRole: editor+Idempotency-Key
status: 'paused' stops serving it (its traffic is redistributed); status: 'active' resumes a paused variant. You can also replace its modifications, hypothesis, or notes. The baseline's status can't be changed; promote a variant instead.
| Field | In | Type | Description |
|---|
page_idrequired | path | string | Page test id (the page_id slug). |
variant_idrequired | path | string | Variant id within a test. |
status | body | "active" | "paused" | |
modifications | body | object[] | |
hypothesis | body | string | |
notes | body | string | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X PATCH "https://api.roas.to/v1/tests/{page_id}/variants/{variant_id}" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"kv": "deployed",
"variant_id": "v2",
"variant_status": "paused"
}
DELETE/v1/tests/{page_id}/variants/{variant_id}
Archive a variant
Scope: write:snippetsRole: editor+Idempotency-Key
Retires a variant (kept for history). The baseline cannot be archived.
| Field | In | Type | Description |
|---|
page_idrequired | path | string | Page test id (the page_id slug). |
variant_idrequired | path | string | Variant id within a test. |
Request
curl -X DELETE "https://api.roas.to/v1/tests/{page_id}/variants/{variant_id}" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"status": "completed",
"kv": "deployed"
}
POST/v1/tests/{page_id}/variants/{variant_id}/promote
Promote a variant to baseline
Scope: write:snippetsRole: editor+Idempotency-Key
Makes the variant the new baseline (the old baseline is archived) at the page's baseline traffic share. Use it when you've seen enough. The optimizer does this automatically when auto-promote is on.
| Field | In | Type | Description |
|---|
page_idrequired | path | string | Page test id (the page_id slug). |
variant_idrequired | path | string | Variant id within a test. |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/tests/{page_id}/variants/{variant_id}/promote" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"kv": "deployed",
"promoted": "v2"
}
Link tests
GET/v1/link-tests
List link tests
Scope: read:snippetsRole: viewer+
Link tests with their public tracking link_url (use it as the ad destination).
| Field | In | Type | Description |
|---|
status | query | "active" | "archived" | "all" | |
limit | query | integer | Rows per page (1–1000, default 100). |
offset | query | integer | Rows to skip. |
Request
curl -X GET "https://api.roas.to/v1/link-tests" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"total": 1,
"limit": 100,
"offset": 0,
"has_more": false,
"link_tests": [
{
"group_id": "keto-split",
"name": "Keto split",
"status": "active",
"link_url": "https://go.example.com/link/keto-split",
"baseline_url": "https://example.com/offer-a",
"baseline_traffic_pct": 60,
"auto_promote_enabled": true,
"auto_pause_enabled": true,
"variant_count": 3,
"created_at": "2026-09-15T10:00:00.000-04:00"
}
]
}
POST/v1/link-tests
Create a link test
Scope: write:snippetsRole: editor+Idempotency-Key
Creates a link test with a baseline destination and optional challengers, splits traffic (baseline_traffic_pct to the baseline, the rest evenly), and deploys it. Returns the tracking link_url. kv: "deploy_failed" means the test was created but not yet published — call POST …/deploy. id becomes part of the URL and must be unique across ROAS.
| Field | In | Type | Description |
|---|
idrequired | body | string | |
namerequired | body | string | |
source_network | body | string | |
baseline_urlrequired | body | string (uri) | |
baseline_destination_typerequired | body | string | |
baseline_destination_id | body | string | |
baseline_traffic_pct | body | number | |
auto_promote_enabled | body | boolean | |
auto_pause_enabled | body | boolean | |
target_test_count | body | integer | |
test_variants | body | object[] | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/link-tests" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"id": "120210000000000001",
"name": "name",
"baseline_url": "https://example.com/offer",
"baseline_destination_type": "baseline destination type"
}'
Response 201
{
"status": "completed",
"kv": "deployed",
"group_id": "keto-split",
"link_url": "https://go.example.com/link/keto-split",
"variants_created": 3,
"traffic_split": {
"baseline": 60,
"per_test": 20
}
}
GET/v1/link-tests/{group_id}
Get a link test with results
Scope: read:snippetsRole: viewer+
The link test plus every variant with traffic share and performance: views, clicks, conversions, revenue, EPC, EPV, and the confidence (0–100) that it earns more per visitor than the baseline.
| Field | In | Type | Description |
|---|
group_idrequired | path | string | |
Request
curl -X GET "https://api.roas.to/v1/link-tests/{group_id}" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"group_id": "keto-split",
"name": "Keto split",
"status": "active",
"link_url": "https://go.example.com/link/keto-split",
"baseline_url": "https://example.com/offer-a",
"baseline_traffic_pct": 60,
"auto_promote_enabled": true,
"auto_pause_enabled": true,
"variant_count": 3,
"created_at": "2026-09-15T10:00:00.000-04:00",
"variants": [
{
"variant_id": "test_1",
"destination": {
"url": "https://example.com/offer-b",
"type": "offer",
"id": null
},
"status": "active",
"stage": "growing",
"traffic_pct": 20,
"hypothesis": "Shorter checkout wins",
"performance": {
"views": 4012,
"clicks": 1170,
"conversions": 49,
"revenue": 2205,
"epc": 1.88,
"epv": 0.55,
"ctr": 29.2,
"cvr": 4.19,
"confidence_vs_baseline": 81.4
}
}
],
"promotions": []
}
DELETE/v1/link-tests/{group_id}
Delete a link test permanently
Scope: write:snippetsRole: admin+Idempotency-Key
Deletes the test and its tracking data. The link stops working. Requires ?confirm=true and the admin role.
| Field | In | Type | Description |
|---|
group_idrequired | path | string | |
confirmrequired | query | "true" | Must be "true". Guards against accidental deletes. |
Request
curl -X DELETE "https://api.roas.to/v1/link-tests/{group_id}?confirm=true" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"status": "completed",
"kv": "cleaned"
}
POST/v1/link-tests/{group_id}/archive
Archive a link test
Scope: write:snippetsRole: editor+Idempotency-Key
Stops the test and removes it from the edge. Data is kept.
| Field | In | Type | Description |
|---|
group_idrequired | path | string | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/link-tests/{group_id}/archive" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"kv": "cleaned"
}
POST/v1/link-tests/{group_id}/unarchive
Restore an archived link test
Scope: write:snippetsRole: editor+Idempotency-Key
Restores the test and redeploys it.
| Field | In | Type | Description |
|---|
group_idrequired | path | string | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/link-tests/{group_id}/unarchive" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"kv": "deployed"
}
POST/v1/link-tests/{group_id}/deploy
Force a redeploy to the edge
Scope: write:snippetsRole: editor+Idempotency-Key
Re-publishes the current split to the edge. Normally automatic.
| Field | In | Type | Description |
|---|
group_idrequired | path | string | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/link-tests/{group_id}/deploy" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"kv": "deployed"
}
POST/v1/link-tests/{group_id}/variants
Add a destination
Scope: write:snippetsRole: editor+Idempotency-Key
Adds a challenger destination (starts as a seedling with a small share).
| Field | In | Type | Description |
|---|
group_idrequired | path | string | |
destination_urlrequired | body | string (uri) | |
destination_type | body | string | |
destination_id | body | string | |
hypothesis | body | string | |
notes | body | string | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/link-tests/{group_id}/variants" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"destination_url": "https://example.com/offer"
}'
Response 201
{
"status": "completed",
"kv": "deployed",
"variant_id": "test_3"
}
PATCH/v1/link-tests/{group_id}/variants/{variant_id}
Edit, pause, or resume a destination
Scope: write:snippetsRole: editor+Idempotency-Key
status: 'paused' | 'active' stops / resumes a challenger (the baseline can't be paused; promote a destination instead). You can also change the destination URL, hypothesis, or notes.
| Field | In | Type | Description |
|---|
group_idrequired | path | string | |
variant_idrequired | path | string | |
status | body | "active" | "paused" | |
destination_url | body | string (uri) | |
hypothesis | body | string | |
notes | body | string | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X PATCH "https://api.roas.to/v1/link-tests/{group_id}/variants/{variant_id}" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"kv": "deployed",
"variant_id": "test_1"
}
DELETE/v1/link-tests/{group_id}/variants/{variant_id}
Archive a destination
Scope: write:snippetsRole: editor+Idempotency-Key
Retires a challenger destination (kept for history).
| Field | In | Type | Description |
|---|
group_idrequired | path | string | |
variant_idrequired | path | string | |
Request
curl -X DELETE "https://api.roas.to/v1/link-tests/{group_id}/variants/{variant_id}" \
-H "Authorization: Bearer $ROAS_API_KEY"
Response 200
{
"status": "completed",
"kv": "deployed"
}
POST/v1/link-tests/{group_id}/variants/{variant_id}/promote
Promote a destination
Scope: write:snippetsRole: editor+Idempotency-Key
mode: 'promote' (default) makes it the new baseline at the test's baseline share and keeps testing; mode: 'winner' sends 100% of traffic to it and pauses the rest.
| Field | In | Type | Description |
|---|
group_idrequired | path | string | |
variant_idrequired | path | string | |
mode | body | "promote" | "winner" | |
created_by | body | string | Free-text attribution (your script, agent, or person). Recorded in the workspace audit log next to the API key. |
Request
curl -X POST "https://api.roas.to/v1/link-tests/{group_id}/variants/{variant_id}/promote" \
-H "Authorization: Bearer $ROAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
Response 200
{
"status": "completed",
"kv": "deployed",
"promoted": "test_1",
"lift_pct": "14.2"
}