REST API

Everything you can do in ROAS.to, from code: read campaign and tracking data, pause, scale and duplicate campaigns, launch new ones, run page and link tests, manage rules, send conversions, and get webhooks when things happen.

Quick start

Create a key in Settings → API Keys, then check what it can do:

curl https://api.roas.to/v1/me -H "Authorization: Bearer $ROAS_API_KEY"

Every endpoint below is also in the machine-readable OpenAPI 3.1 document at https://api.roas.to/v1/openapi.json. Import it into Postman, generate an SDK, or hand it to an AI agent as its tool list.

Keys & scopes

Send the key as Authorization: Bearer roas_… (or x-api-key). A key acts as the person who created it: their role (viewer, editor, admin, owner) and the ad accounts they can access. Scopes narrow it further. A key can never do more than its creator.

  • read:campaignsRead ad accounts, campaigns, ad sets, ads, creatives, launch history, audiences, pixels, pages, and media.
  • read:analyticsRead performance analytics: daily, hourly, funnel, placements, profitability, account health, and data freshness.
  • read:conversionsRead attributed conversions and offer performance.
  • read:snippetsRead page tests, link tests, and landing-page experiments — including verdicts, beat-baseline confidence, snippet health, and selector drift.
  • read:automationRead automation, clone, and auto-boost rules, their execution history, budget alerts, and pending approvals.
  • write:statusPause and activate campaigns, ad sets, and ads.
  • write:budgetsScale or set campaign and ad set budgets.
  • write:bidsUpdate bid amounts where supported.
  • write:cloneDuplicate campaigns (with optional overrides).
  • write:createLaunch campaigns, ad sets, and ads from scratch, edit schedules and names, and upload media.
  • write:deleteDelete or archive campaigns, ad sets, and ads.
  • write:automationCreate, edit, assign, delete, and pause/resume automation, clone, and auto-boost rules.
  • write:snippetsCreate and manage page tests and link tests: variants, traffic split, promotion, settings, and archiving.
  • write:conversionsSend conversions (sales) from your own server.
  • read:webhooksList webhook endpoints and the webhook delivery log.
  • write:webhooksCreate, edit, delete, and send test events to webhook endpoints.

Writes, approvals, retries

  • Idempotency-Key header on any write: a retry with the same key and body returns the original response (Idempotent-Replay: true) instead of acting twice. Keys live 24 hours. Same key + different body → 422.
  • dry_run: true validates and previews the exact change (current → new values) without touching Meta.
  • created_by (any write) records who or what made the change in the workspace activity log, next to the key.
  • Approval rules: if your workspace requires approval for a change, the API answers 202 with pending_action_id. An owner or admin approves it in ROAS.to; poll GET /v1/approvals/{id} for the outcome.
  • Facebook writes re-read the entity from Meta and return what Meta holds under verified.

Units, dates, pages

  • Money you send is in major units of the ad account currency (dollars, not cents). Response fields ending _minor are integer minor units.
  • Timestamps come back in the workspace timezone (see the X-Timezone header). Ranges: date_range (e.g. last_7d) or start_date + end_date (YYYY-MM-DD, inclusive).
  • Lists take limit + offset and return total and has_more.
  • Facebook ids are numeric strings; ad accounts look like act_123….

Errors & limits

Every error is { "error": { "code", "message" } }, and every response carries an X-Request-Id to quote to support. Limits: 300 reads and 60 writes per minute per key, reported in X-RateLimit-* headers.

StatusCodeMeaning
400invalid_requestValidation failed. problems[] lists every issue (path + message).
401unauthorizedMissing, invalid, expired, or revoked key, or an inactive plan.
403forbiddenMissing scope, creator role too low, or the ad account is outside the creator's access.
404not_foundUnknown endpoint or entity.
409conflictConcurrent change, or an Idempotency-Key still in flight.
422unprocessableMeta (or a preflight) rejected the action; or an Idempotency-Key reused with a different body.
429rate_limit_exceededSlow down; retry after the Retry-After header.
502 / 504upstream_errorMeta or an upstream service didn't answer. Retry writes with the same Idempotency-Key.
503service_unavailableMeta rate budget exhausted for the ad account, or a dependency is down. Retry shortly.

Webhooks

Register an https endpoint (POST /v1/webhooks/endpoints or Settings → Integrations) and ROAS.to POSTs signed events to it. Each event is { id, type, timestamp, data }; retries keep the same id.

  • test.variant.promotedA winning variant became the new baseline.
  • test.variant.pausedA losing variant was paused by the optimizer.
  • test.verdict.winnerThe engine reached statistical confidence in a winner.
  • test.verdict.futilityThe engine concluded the variants are not meaningfully different.
  • test.variant.createdAuto-generation added a new variant to a page.
  • snippet.went_darkA previously live snippet stopped reporting visitors.
  • launch.completedA campaign launch finished (from the app, the API, MCP, clone rules, or automation) — includes ads created and any per-ad failures. A slow launch first reported as launch.failed (timed out) can still finish; it then sends launch.completed for the same launch_id.
  • launch.failedA campaign launch failed — includes the plain-English reason and Meta error detail.
  • clone.completedA clone rule duplicated a campaign.
  • clone.failedA clone rule tried to duplicate a campaign and failed.
  • automation.action_executedAn automation rule changed something: paused / activated, scaled a budget, set a bid cap, or queued a duplicate.
  • conversion.recordedA new sale was recorded (postback, pixel, Shopify, tracker sync, or the API) — with its attribution.
  • account.access_lostROAS lost access to an ad account (it was unshared or the connection was revoked).
  • budget_alert.createdThe budget optimizer flagged a scale opportunity, reallocation, degradation, or new winner.

Verify signatures with any Standard Webhooks library:

import { Webhook } from 'standardwebhooks';

// secret = the whsec_… value returned when you created the endpoint
const wh = new Webhook(secret.replace(/^whsec_/, ''));

app.post('/hooks/roas', express.raw({ type: 'application/json' }), (req, res) => {
  const event = wh.verify(req.body, {
    'webhook-id': req.header('webhook-id'),
    'webhook-timestamp': req.header('webhook-timestamp'),
    'webhook-signature': req.header('webhook-signature'),
  }); // throws if the signature is wrong
  // event.id is stable across retries: dedupe on it
  handle(event.type, event.data);
  res.sendStatus(200);
});

Account

GET/v1

API index

Scope: any keyRole: viewer+

Version, docs links, rate limits, and every endpoint with its required scope.

Request

curl -X GET "https://api.roas.to/v1" \
  -H "Authorization: Bearer $ROAS_API_KEY"

Response 200

{
  "name": "ROAS.to API",
  "version": "v1",
  "documentation": "https://www.roas.to/docs/api",
  "openapi": "https://api.roas.to/v1/openapi.json",
  "rate_limits": {
    "reads_per_minute": 300,
    "writes_per_minute": 60
  },
  "endpoints": [
    {
      "method": "GET",
      "path": "/v1/me",
      "scope": [],
      "summary": "Who am I"
    }
  ]
}
GET/v1/me

Who am I

Scope: any keyRole: viewer+

The workspace, key, scopes, and role this key acts with, plus the ad accounts it can see (null = every account). Use it to check a key's access before calling anything else.

Request

curl -X GET "https://api.roas.to/v1/me" \
  -H "Authorization: Bearer $ROAS_API_KEY"

Response 200

{
  "workspace": {
    "id": "0d4c1a52-7b2e-4c3d-9e8f-1a2b3c4d5e6f",
    "name": "Acme Media",
    "timezone": "America/New_York"
  },
  "key": {
    "id": "7e1f2a3b-4c5d-4e6f-8a9b-0c1d2e3f4a5b",
    "name": "Agent",
    "prefix": "roas_1a2b3"
  },
  "acting_as": {
    "role": "admin",
    "user_id": "user_2abc"
  },
  "scopes": [
    "read:campaigns",
    "write:status"
  ],
  "account_scope": null,
  "rate_limits": {
    "reads_per_minute": 300,
    "writes_per_minute": 60
  }
}
GET/v1/activity

Workspace activity log

Scope: read:automationRole: admin+

Who changed what, newest first: launches, edits, rule changes, approvals, API and MCP actions. Admin role and an unrestricted key required.

FieldInTypeDescription
entity_typequerystring
actionquerystring
user_idquerystring
fromquerystring (date)
toquerystring (date)
limitqueryintegerRows per page (1–1000, default 100).
offsetqueryintegerRows to skip.

Request

curl -X GET "https://api.roas.to/v1/activity" \
  -H "Authorization: Bearer $ROAS_API_KEY"

Response 200

{
  "total": 1,
  "limit": 100,
  "offset": 0,
  "has_more": false,
  "entries": [
    {
      "id": "6f7a8b9c-0d1e-4f2a-8b3c-4d5e6f7a8b9c",
      "user_id": "user_2abc",
      "action": "executed",
      "entity_type": "api_key",
      "entity_id": "7e1f…",
      "summary": "API launchCampaign: Launched campaign \"TOF — Broad — US\" (6 ads)",
      "created_at": "2026-10-19T09:00:41.000-04:00"
    }
  ]
}

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.

FieldInTypeDescription
account_idrequiredpathstringFacebook ad account id, e.g. act_1234567890.
fee_pctrequiredbodynumberSurcharge 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.

FieldInTypeDescription
account_idquerystringFacebook ad account id, e.g. act_1234567890.
statusquerystringFilter by status (ACTIVE, PAUSED, …). ACTIVE means currently serving.
min_spendquerynumberOnly campaigns that spent at least this much (account currency) in the date range.
has_spendquery"true" | "false"true: only campaigns with any spend in the date range.
date_rangequery"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_datequerystring (date)YYYY-MM-DD (inclusive). Use with end_date.
end_datequerystring (date)YYYY-MM-DD (inclusive). Max range 548 days. Ignored when date_range is set.
limitqueryintegerRows per page (1–1000, default 100).
offsetqueryintegerRows 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.

FieldInTypeDescription
campaign_idrequiredpathstringNumeric Facebook id (campaign / ad set / ad).
date_rangequery"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_datequerystring (date)YYYY-MM-DD (inclusive). Use with end_date.
end_datequerystring (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.

FieldInTypeDescription
campaign_idquerystringNumeric Facebook id (campaign / ad set / ad).
account_idquerystringFacebook ad account id, e.g. act_1234567890.
statusquerystringFilter by status (ACTIVE, PAUSED, …). ACTIVE means currently serving.
date_rangequery"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_datequerystring (date)YYYY-MM-DD (inclusive). Use with end_date.
end_datequerystring (date)YYYY-MM-DD (inclusive). Max range 548 days. Ignored when date_range is set.
limitqueryintegerRows per page (1–1000, default 100).
offsetqueryintegerRows 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.

FieldInTypeDescription
adset_idrequiredpathstringNumeric Facebook id (campaign / ad set / ad).
date_rangequery"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_datequerystring (date)YYYY-MM-DD (inclusive). Use with end_date.
end_datequerystring (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.

FieldInTypeDescription
campaign_idquerystringNumeric Facebook id (campaign / ad set / ad).
adset_idquerystringNumeric Facebook id (campaign / ad set / ad).
account_idquerystringFacebook ad account id, e.g. act_1234567890.
statusquerystringFilter by status (ACTIVE, PAUSED, …). ACTIVE means currently serving.
date_rangequery"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_datequerystring (date)YYYY-MM-DD (inclusive). Use with end_date.
end_datequerystring (date)YYYY-MM-DD (inclusive). Max range 548 days. Ignored when date_range is set.
limitqueryintegerRows per page (1–1000, default 100).
offsetqueryintegerRows 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.

FieldInTypeDescription
ad_idrequiredpathstringNumeric Facebook id (campaign / ad set / ad).
date_rangequery"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_datequerystring (date)YYYY-MM-DD (inclusive). Use with end_date.
end_datequerystring (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.

FieldInTypeDescription
account_idquerystringFacebook ad account id, e.g. act_1234567890.
campaign_idquerystringNumeric Facebook id (campaign / ad set / ad).
ad_idquerystringNumeric Facebook id (campaign / ad set / ad).
date_rangequery"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_datequerystring (date)YYYY-MM-DD (inclusive). Use with end_date.
end_datequerystring (date)YYYY-MM-DD (inclusive). Max range 548 days. Ignored when date_range is set.
limitqueryintegerRows per page (1–1000, default 100).
offsetqueryintegerRows 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.

FieldInTypeDescription
account_idquerystringFacebook ad account id, e.g. act_1234567890.
campaign_idquerystringNumeric Facebook id (campaign / ad set / ad).
date_rangequery"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_datequerystring (date)YYYY-MM-DD (inclusive). Use with end_date.
end_datequerystring (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.

FieldInTypeDescription
account_idquerystringFacebook ad account id, e.g. act_1234567890.
campaign_idquerystringNumeric Facebook id (campaign / ad set / ad).
limitqueryintegerRows per page (1–1000, default 100).
offsetqueryintegerRows 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.

FieldInTypeDescription
account_idquerystringFacebook ad account id, e.g. act_1234567890.
limitqueryintegerRows 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.

FieldInTypeDescription
account_idquerystringFacebook ad account id, e.g. act_1234567890.
campaign_idquerystringNumeric Facebook id (campaign / ad set / ad).
date_rangequery"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_datequerystring (date)YYYY-MM-DD (inclusive). Use with end_date.
end_datequerystring (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.

FieldInTypeDescription
datequerystring (date)One exact day (YYYY-MM-DD).
campaign_idquerystringNumeric Facebook id (campaign / ad set / ad).
campaign_searchquerystringCampaign name contains.
date_rangequery"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_datequerystring (date)YYYY-MM-DD, use with end_date.
end_datequerystring (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).

FieldInTypeDescription
group_byquery"campaign" | "day" | "hour"
campaign_searchquerystring
start_hourqueryinteger
end_hourqueryinteger
date_rangequery"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_datequerystring (date)YYYY-MM-DD, use with end_date.
end_datequerystring (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.

FieldInTypeDescription
account_idquerystringFacebook ad account id, e.g. act_1234567890.
campaign_idquerystringNumeric Facebook id (campaign / ad set / ad).
statusquery"active" | "all"
date_rangequery"today" | "yesterday" | "last_3d" | "last_7d" | "last_14d" | "last_30d" | "this_month" | "all_time"
start_datequerystring (date)
end_datequerystring (date)
limitqueryintegerRows (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.

FieldInTypeDescription
account_searchquerystring
include_assetsquery"true" | "false"
limitqueryintegerRows (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.

FieldInTypeDescription
account_searchquerystring
limitqueryintegerRows (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.

FieldInTypeDescription
severityquery"info" | "warning" | "critical" | "all"
alert_typequery"scale_opportunity" | "reallocation" | "degradation" | "new_winner" | "all"
include_dismissedquery"true" | "false"
campaign_searchquerystring
date_rangequery"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_datequerystring (date)YYYY-MM-DD, use with end_date.
end_datequerystring (date)YYYY-MM-DD (inclusive).
limitqueryintegerRows (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).

FieldInTypeDescription
searchquerystring
statusquery"ACTIVE" | "PAUSED" | "DELETED" | "ARCHIVED" | "all"
sort_byquery"spend" | "revenue" | "roas" | "clicks" | "impressions"
date_rangequery"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_datequerystring (date)YYYY-MM-DD, use with end_date.
end_datequerystring (date)YYYY-MM-DD (inclusive).
limitqueryintegerRows (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.

FieldInTypeDescription
group_byquery"creative" | "headline" | "image" | "body_text" | "ad"
campaign_idquerystringNumeric Facebook id (campaign / ad set / ad).
campaign_searchquerystring
date_rangequery"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_datequerystring (date)YYYY-MM-DD, use with end_date.
end_datequerystring (date)YYYY-MM-DD (inclusive).
limitqueryintegerRows (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 launch

GET/v1/facebook/launches

List launches (including failed)

Scope: read:campaignsRole: viewer+

Every launch attempt (from the app, the API, MCP, clone rules, and automation), newest first, with status creating, completed, or failed and the plain-English failure reason.

FieldInTypeDescription
account_idquerystringFacebook ad account id, e.g. act_1234567890.
statusquery"creating" | "completed" | "failed"
limitqueryintegerRows per page (1–1000, default 100).
offsetqueryintegerRows to skip.

Request

curl -X GET "https://api.roas.to/v1/facebook/launches" \
  -H "Authorization: Bearer $ROAS_API_KEY"

Response 200

{
  "total": 1,
  "limit": 100,
  "offset": 0,
  "has_more": false,
  "launches": [
    {
      "launch_id": "b9a7c1d2-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
      "status": "failed",
      "fb_account_id": "act_123456789",
      "campaign_id": null,
      "campaign_name": "TOF — Broad — US",
      "launched_via": "api",
      "source_label": "API",
      "ads_created": 0,
      "failure_reason": "Page can't run ads. Connect the Page to this ad account's business, then retry.",
      "created_at": "2026-10-19T09:00:00.000-04:00",
      "failed_at": "2026-10-19T09:00:12.000-04:00"
    }
  ]
}
GET/v1/facebook/launches/{launch_id}

Get a launch

Scope: read:campaignsRole: viewer+

One launch with its full outcome: created campaign / ad set / ad ids, per-ad failures, and Meta’s error detail (code, subcode, raw message) when it failed.

FieldInTypeDescription
launch_idrequiredpathstring (uuid)

Request

curl -X GET "https://api.roas.to/v1/facebook/launches/{launch_id}" \
  -H "Authorization: Bearer $ROAS_API_KEY"

Response 200

{
  "launch_id": "b9a7c1d2-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
  "status": "failed",
  "fb_account_id": "act_123456789",
  "campaign_id": null,
  "campaign_name": "TOF — Broad — US",
  "launched_via": "api",
  "source_label": "API",
  "ads_created": 0,
  "failure_reason": "Page can't run ads. Connect the Page to this ad account's business, then retry.",
  "created_at": "2026-10-19T09:00:00.000-04:00",
  "failed_at": "2026-10-19T09:00:12.000-04:00",
  "adset_id": null,
  "ad_ids": [],
  "failure_detail": {
    "kind": "permission",
    "code": 200,
    "subcode": 1487194,
    "raw_message": "…",
    "failed_ads": []
  }
}
POST/v1/facebook/launch

Launch a campaign from scratch

Scope: write:createRole: editor+dry_runapproval rulesIdempotency-Key

Builds a campaign → ad set(s) → ads in one call, with the same engine as the Create Ads page. Every headline × body × media combination becomes an ad (or one ad per media item with ad_mode: 'per_creative'). Tracking is stamped on every ad automatically — there is no url_tags to pass: every ad's link carries sub1 = campaign id, sub2 = ad set id, sub3 = the ad (its Meta ad id, or a ROAS ad_… id ROAS resolves back to it), and the tracking redirect appends sub1–sub5 to your offer URL, so Everflow (or any tracker reading sub1–sub3) receives them with no setup. Don't add your own {{campaign.id}}-style params to destination_url — duplicate sub params make attribution ambiguous.

  • Media: upload first with POST /v1/media/images / POST /v1/media/videos, then pass the returned ids in image_ids.
  • Destination: destination_url or page_test_id.
  • Created PAUSED unless start_immediately (or start_time) is set.
  • Use dry_run: true to validate and preview the build without touching Meta.
  • Pass an Idempotency-Key header: a retry returns the original launch instead of creating a second campaign.

Runs synchronously (typically 15–60s). Partial success returns 201 with ads_failed > 0 and the per-ad reasons. A failed launch returns 422 with the reason and the launch_id to inspect via GET /v1/facebook/launches/{launch_id}.

FieldInTypeDescription
fb_account_idrequiredbodystringAd account id (e.g. act_123…). REST: GET /v1/facebook/accounts · MCP: list_ad_assets.
campaign_namerequiredbodystringName for the new campaign.
fb_page_idrequiredbodystringFacebook Page id to run the ads under.
fb_pixel_idrequiredbodystringFacebook Pixel id for conversion tracking.
daily_budget_majorrequiredbodynumberDaily budget in MAJOR account currency (e.g. dollars).
headlinesrequiredbodystring[]1–5 ad headlines.
body_textsrequiredbodystring[]1–5 primary/body texts.
image_idsrequiredbodystring (uuid)[]Media-library ids (UUIDs) for images OR videos. REST: GET /v1/media, POST /v1/media/images|videos · MCP: list_media, upload_media_from_url, upload_video_from_url.
descriptionsbodystring[]Optional link descriptions.
video_thumbnail_idsbodystring (uuid)[]Optional custom thumbnail image ids, parallel to image_ids (for video creatives).
custom_ctabodystringCall-to-action (e.g. SHOP_NOW, LEARN_MORE).
countriesrequiredbodystring[]ISO country codes (e.g. ['US','CA']) or ['WORLDWIDE'].
age_minbodyinteger
age_maxbodyinteger
gendersbody"1" | "2" | "all"[]
devicesbody"mobile" | "desktop"[]
exclude_tabletbodybooleanOpt out of tablets (deliver to phones only). Meta has no separate tablet placement, so this excludes tablet devices from the mobile bucket. Desktop is unaffected.
placementsbodystring[]Placement tokens; omit to use the standard default set.
campaign_objectivebodystringDefaults to the standard objective.
conversion_eventbody"PURCHASE" | "ADD_TO_CART" | "INITIATED_CHECKOUT" | "LEAD" | "COMPLETE_REGISTRATION" | "CONTENT_VIEW" | "ADD_PAYMENT_INFO" | "ADD_TO_WISHLIST" | "SUBSCRIBE" | "START_TRIAL" | "CONTACT" | "DONATE" | "SCHEDULE" | "SUBMIT_APPLICATION" | "FIND_LOCATION" | "SEARCH"Pixel conversion event to optimize for (defaults to PURCHASE). Meta rejects events that are invalid for the chosen campaign objective (this is enforced by Meta at create time, not pre-validated here) — e.g. LEAD for lead-gen, ADD_TO_CART / INITIATED_CHECKOUT / COMPLETE_REGISTRATION for sales funnels.
optimization_goalbodystring
bid_strategybodystring
advantage_audiencebodyboolean
advantage_placementsbodyboolean
advantage_creativebodyboolean
ai_disclosurebodybooleanManual "media created or edited with AI" self-disclosure stamped on every creative (legal compliance, e.g. NY State). Set true when the ad media is AI-generated or AI-edited.
value_rule_setbodyobjectOptional Value Rules — bid multipliers by audience / placement / conversion-location criteria within the ad set ({name, rules[]}). Eligible only with LOWEST_COST_WITHOUT_CAP or COST_CAP bid strategy. Best-effort: ignored (campaign still launches) if the account can't use value rules.
value_rule_recipe_idbodystring (uuid)Optional id of a saved value-rule recipe this set came from (UI/reuse pointer).
destination_urlbodystring (uri)Where the ads send people (your landing page or offer URL). Required unless page_test_id is set. Sub-id tracking parameters are appended automatically.
page_test_idbodystringSend traffic through a ROAS page test instead of a raw URL (the test page_id). Required unless destination_url is set.
offer_namebodystringOptional label for the destination offer (shown in reporting).
ad_modebody"combinatorial" | "per_creative"'combinatorial' (default) builds every headline × body × media combination; 'per_creative' builds one ad per media item sharing the same copy variants.
ads_per_ad_setbodyintegerSplit the ads across multiple ad sets with at most this many ads each.
bid_amount_majorbodynumberBid cap / cost cap amount in MAJOR account currency. Required when bid_strategy is a cap strategy (LOWEST_COST_WITH_BID_CAP or COST_CAP). With LOWEST_COST_WITH_MIN_ROAS it is the ROAS floor as a multiple (2 = 2.0x), not money.
start_timebodystring (date-time)Schedule the launch: ISO-8601 datetime the ad set starts delivering. Implies the campaign is created ACTIVE but waits for this time.
end_datebodystring (date)Ad set end date (YYYY-MM-DD, end of day in the ad account timezone).
interest_idsbodystring[]Meta interest ids to target. Find them with GET /v1/facebook/targeting/interests.
interest_namesbodystring[]Display names for interest_ids (same order). Optional, used for reporting.
included_audience_idsbodystring[]Custom/lookalike audience ids to target. See GET /v1/facebook/audiences.
excluded_audience_idsbodystring[]Custom audience ids to exclude.
languagesbodyinteger[]Meta locale ids to target (e.g. 6 = English US, 24 = English UK).
special_ad_categoriesbody"HOUSING" | "EMPLOYMENT" | "ISSUES_ELECTIONS_POLITICS" | "FINANCIAL_PRODUCTS_SERVICES" | "ONLINE_GAMBLING_AND_GAMING"[]Declare a special ad category (credit, employment, housing, …) when required.
special_ad_category_countriesbodystring[]Countries the special ad category applies in (ISO codes).
custom_conversion_idbodystringOptimize for a Meta custom conversion instead of conversion_event.
automation_rule_idsbodystring (uuid)[]Automation rules to attach to the new campaign at launch.
start_immediatelybodybooleanIf true, the campaign launches ACTIVE; otherwise PAUSED.
dry_runbodybooleanValidate and preview the exact change without applying it. Nothing is sent to Meta.
created_bybodystringFree-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/launch" \
  -H "Authorization: Bearer $ROAS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "fb_account_id": "act_123456789",
    "campaign_name": "campaign name",
    "fb_page_id": "120210000000000001",
    "fb_pixel_id": "120210000000000001",
    "daily_budget_major": 50,
    "headlines": [
      "headline"
    ],
    "body_texts": [
      "body text"
    ],
    "image_ids": [
      "00000000-0000-4000-8000-000000000000"
    ],
    "countries": [
      "countrie"
    ]
  }'

Response 201

{
  "status": "completed",
  "campaign_id": "120210000000000099",
  "launch_id": "b9a7c1d2-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
  "adset_ids": [
    "120210000000000100"
  ],
  "ad_ids": [
    "120210000000000101",
    "120210000000000102"
  ],
  "ads_created": 2,
  "ads_failed": 0,
  "failed_ads": [],
  "warnings": []
}

Facebook assets

GET/v1/facebook/creatives/search

Search creatives

Scope: read:campaignsRole: viewer+

Find ads by text in their headline, body, or name.

FieldInTypeDescription
qrequiredquerystring
account_idquerystringFacebook ad account id, e.g. act_1234567890.
limitqueryintegerRows (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.

FieldInTypeDescription
ad_idrequiredpathstringNumeric Facebook id (campaign / ad set / ad).
ad_formatquerystring

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.

FieldInTypeDescription
typequery"custom" | "saved" | "all"
account_idquerystringFacebook ad account id, e.g. act_1234567890.
searchquerystring
include_inactivequery"true" | "false"
limitqueryintegerRows (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.

FieldInTypeDescription
account_idquerystringFacebook ad account id, e.g. act_1234567890.
searchquerystring
include_inactivequery"true" | "false"
limitqueryintegerRows (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.

FieldInTypeDescription
account_idquerystringFacebook ad account id, e.g. act_1234567890.
searchquerystring
limitqueryintegerRows per page (1–1000, default 100).
offsetqueryintegerRows 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.

FieldInTypeDescription
qrequiredquerystring
account_idrequiredquerystringAd account whose access is used for the lookup.
limitqueryintegerRows (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.

FieldInTypeDescription
typerequiredbody"campaign" | "adset" | "ad"Entity level the ids refer to: 'campaign', 'adset', or 'ad'.
idsrequiredbodystring[]Up to 50 Facebook ids of the given type.
statusrequiredbody"ACTIVE" | "PAUSED"Target status.
fb_account_idbodystringAd account that owns the ids. Optional: inferred from the ids when omitted.
dry_runbodybooleanValidate and preview the exact change without applying it. Nothing is sent to Meta.
created_bybodystringFree-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.

FieldInTypeDescription
typerequiredbody"campaign" | "adset"'campaign' (CBO) or 'adset' (ABO).
updatesrequiredbodyobject[]
fb_account_idbodystringAd account that owns the ids. Optional: inferred from the ids when omitted.
dry_runbodybooleanValidate and preview the exact change without applying it. Nothing is sent to Meta.
created_bybodystringFree-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.

FieldInTypeDescription
adset_idsrequiredbodystring[]Up to 50 ad set ids (all in one ad account).
bid_amountrequiredbodynumberBid amount, MAJOR account currency.
dry_runbodybooleanValidate and preview the exact change without applying it. Nothing is sent to Meta.
created_bybodystringFree-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.

FieldInTypeDescription
typerequiredbody"campaign" | "adset" | "ad"Entity level the ids refer to: 'campaign', 'adset', or 'ad'.
idsrequiredbodystring[]Up to 50 Facebook ids of the given type.
modebody"delete" | "archive"'delete' removes permanently; 'archive' keeps it in Meta's archive.
fb_account_idbodystringAd account that owns the ids. Optional: inferred from the ids when omitted.
confirmrequiredbodytrueMust be true. Guards against accidental destructive calls.
dry_runbodybooleanValidate and preview the exact change without applying it. Nothing is sent to Meta.
created_bybodystringFree-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.

FieldInTypeDescription
campaign_idrequiredpathstringNumeric Facebook id (campaign / ad set / ad).
namebodystring
statusbody"ACTIVE" | "PAUSED"
daily_budgetbodynumber
dry_runbodybooleanValidate and preview the exact change without applying it. Nothing is sent to Meta.
created_bybodystringFree-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.

FieldInTypeDescription
adset_idrequiredpathstringNumeric Facebook id (campaign / ad set / ad).
namebodystring
statusbody"ACTIVE" | "PAUSED"
daily_budgetbodynumber
bid_amountbodynumber
start_timebodystring (date-time)
end_timebodystring (date-time) | null
dry_runbodybooleanValidate and preview the exact change without applying it. Nothing is sent to Meta.
created_bybodystringFree-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.

FieldInTypeDescription
ad_idrequiredpathstringNumeric Facebook id (campaign / ad set / ad).
namebodystring
statusbody"ACTIVE" | "PAUSED"
dry_runbodybooleanValidate and preview the exact change without applying it. Nothing is sent to Meta.
created_bybodystringFree-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.

FieldInTypeDescription
campaign_idrequiredpathstringNumeric Facebook id (campaign / ad set / ad).
daily_budgetbodynumberNew daily budget (MAJOR currency). Omit to copy the original.
page_idbodystringRun under a different Facebook Page.
start_immediatelybodybooleanLaunch ACTIVE (default: created PAUSED).
copy_rulesbodybooleanAttach 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.
headlinesbodystring[]
body_textsbodystring[]
descriptionsbodystring[]
image_idsbodystring (uuid)[]Replace the media with these media-library ids (see GET /v1/media).
video_thumbnail_idsbodystring (uuid) | null[]
custom_ctabodystring
countriesbodystring[]
age_minbodyinteger
age_maxbodyinteger
gendersbody"1" | "2" | "all"[]
devicesbody"mobile" | "desktop"[]
exclude_tabletbodyboolean
placementsbodystring[]
campaign_objectivebodystring
optimization_goalbodystring
bid_strategybodystring
advantage_audiencebodyboolean
advantage_placementsbodyboolean
advantage_creativebodyboolean
ai_disclosurebodyboolean
value_rule_setbodyobject
value_rule_recipe_idbodystring (uuid)
end_datebodystring (date)Ad set end date (YYYY-MM-DD).
dry_runbodybooleanValidate and preview the exact change without applying it. Nothing is sent to Meta.
created_bybodystringFree-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
}

Media

GET/v1/media

List media

Scope: read:campaignsRole: viewer+

Images and videos in the media library, newest first. Pass the id values as image_ids when launching or duplicating.

FieldInTypeDescription
media_typequery"image" | "video"
qquerystringSearch filename and tags.
tagquerystringOnly media with this tag.
limitqueryintegerRows per page (1–1000, default 100).
offsetqueryintegerRows to skip.

Request

curl -X GET "https://api.roas.to/v1/media" \
  -H "Authorization: Bearer $ROAS_API_KEY"

Response 200

{
  "total": 1,
  "limit": 100,
  "offset": 0,
  "has_more": false,
  "media": [
    {
      "id": "6a1f0b2c-3d4e-4f5a-8b6c-7d8e9f0a1b2c",
      "media_type": "image",
      "filename": "hero-v2.jpg",
      "url": "https://imagedelivery.net/abc/6a1f…/public",
      "type": "ad_creative",
      "tags": [
        "keto",
        "ugc"
      ],
      "width": 1080,
      "height": 1350,
      "stream_uid": null,
      "created_at": "2026-10-18T14:02:11.000-04:00"
    }
  ]
}
GET/v1/media/{media_id}

Get a media item

Scope: read:campaignsRole: viewer+

One media item. For videos, status is checked live: ready once transcoding finished, processing while it runs, error if the source could not be transcoded.

FieldInTypeDescription
media_idrequiredpathstring (uuid)

Request

curl -X GET "https://api.roas.to/v1/media/{media_id}" \
  -H "Authorization: Bearer $ROAS_API_KEY"

Response 200

{
  "id": "6a1f0b2c-3d4e-4f5a-8b6c-7d8e9f0a1b2c",
  "media_type": "image",
  "filename": "hero-v2.jpg",
  "url": "https://imagedelivery.net/abc/6a1f…/public",
  "type": "ad_creative",
  "tags": [
    "keto",
    "ugc"
  ],
  "width": 1080,
  "height": 1350,
  "stream_uid": null,
  "created_at": "2026-10-18T14:02:11.000-04:00",
  "status": "ready"
}
POST/v1/media/images

Import an image from a URL

Scope: write:createRole: editor+Idempotency-Key

Fetches a public https image and stores it in the media library. Returns the id to use in image_ids. JPG / PNG / GIF / WebP.

FieldInTypeDescription
urlrequiredbodystring (uri)Public https URL of the image.
namebodystringDisplay name in the library.
typebody"ad_creative" | "product" | "header" | "icon" | "background"
tagsbodystring[]
created_bybodystringFree-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/media/images" \
  -H "Authorization: Bearer $ROAS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "url": "https://example.com/offer"
  }'

Response 201

{
  "status": "completed",
  "id": "6a1f0b2c-3d4e-4f5a-8b6c-7d8e9f0a1b2c",
  "url": "https://imagedelivery.net/abc/6a1f…/public",
  "media_type": "image"
}
POST/v1/media/videos

Import a video from a URL

Scope: write:createRole: editor+Idempotency-Key

Copies a public https video (mp4 / mov) into the media library. Videos transcode asynchronously: when status is processing, poll GET /v1/media/{id} until it's ready (usually under a minute) before launching with it.

FieldInTypeDescription
urlrequiredbodystring (uri)Public https URL of the video.
namebodystring
tagsbodystring[]
created_bybodystringFree-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/media/videos" \
  -H "Authorization: Bearer $ROAS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "url": "https://example.com/offer"
  }'

Response 201

{
  "status": "processing",
  "id": "9b8c7d6e-5f4a-4b3c-8d2e-1f0a9b8c7d6e",
  "media_type": "video",
  "stream_uid": "31c9291ab41fac05471db4e73aa11717"
}

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).

FieldInTypeDescription
account_idquerystringFacebook ad account id, e.g. act_1234567890.
activequery"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.

FieldInTypeDescription
fb_account_idbodystring | null
fb_account_idsbodystring[] | null
namerequiredbodystring
descriptionbodystring | null
rule_groupbodystring | null
data_intervalbody"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_daysbodyinteger | integer[] | null
conditionsbodyobject[] | null
rule_typebodystring
action_typerequiredbody"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_paramsbodyobject
apply_tobody"all" | "campaign" | "adset" | "ad"
campaign_idsbodystring[] | null
adset_idsbodystring[] | null
schedulebody"every_5m" | "every_15m" | "every_30m" | "hourly" | "every_2h" | "every_4h" | "every_6h" | "daily" | "daily_12pm_utc"
cooldown_minutesbody0 | 5 | 15 | 30 | 60 | 120 | 240 | 360 | 720 | 1440 | 2880 | 10080
max_actions_per_daybodyinteger | null
post_failure_cooldown_minutesbody0 | 5 | 15 | 30 | 60 | 120 | 240 | 360 | 720 | 1440 | 2880 | 10080 | null
execute_modebody"execute_and_alert" | "execute" | "alert_only"
execute_when_pausedbodyboolean
active_hoursbodyinteger[] | null
active_daysbodyinteger[] | null
daypart_timezonebodystring | null
daypart_window_startbodystring | null
daypart_window_endbodystring | null
daypart_window_modebody"active" | "pause" | null
applies_to_boostsbodyboolean
is_activebodyboolean
created_bybodystringFree-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+

One automation rule.

FieldInTypeDescription
rule_idrequiredpathstring (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).

FieldInTypeDescription
rule_idrequiredpathstring (uuid)
namebodystring
descriptionbodystring | null
rule_groupbodystring | null
data_intervalbody"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_daysbodyinteger | integer[] | null
conditionsbodyobject[] | null
rule_typebodystring
action_typebody"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_paramsbodyobject
apply_tobody"all" | "campaign" | "adset" | "ad"
campaign_idsbodystring[] | null
adset_idsbodystring[] | null
schedulebody"every_5m" | "every_15m" | "every_30m" | "hourly" | "every_2h" | "every_4h" | "every_6h" | "daily" | "daily_12pm_utc"
cooldown_minutesbody0 | 5 | 15 | 30 | 60 | 120 | 240 | 360 | 720 | 1440 | 2880 | 10080
max_actions_per_daybodyinteger | null
post_failure_cooldown_minutesbody0 | 5 | 15 | 30 | 60 | 120 | 240 | 360 | 720 | 1440 | 2880 | 10080 | null
execute_modebody"execute_and_alert" | "execute" | "alert_only"
execute_when_pausedbodyboolean
active_hoursbodyinteger[] | null
active_daysbodyinteger[] | null
daypart_timezonebodystring | null
daypart_window_startbodystring | null
daypart_window_endbodystring | null
daypart_window_modebody"active" | "pause" | null
applies_to_boostsbodyboolean
is_activebodyboolean
created_bybodystringFree-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.

FieldInTypeDescription
rule_idrequiredpathstring (uuid)
confirmrequiredquery"true"Must be "true". Guards against accidental deletes.
forcequery"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.

FieldInTypeDescription
rule_idrequiredpathstring (uuid)
activerequiredbodybooleantrue = rule runs; false = paused.
created_bybodystringFree-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.

FieldInTypeDescription
rule_idrequiredpathstring (uuid)
campaign_idsrequiredbodystring[]
created_bybodystringFree-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).

FieldInTypeDescription
rule_idrequiredpathstring (uuid)
campaign_idrequiredpathstringNumeric 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.

FieldInTypeDescription
rule_idrequiredpathstring (uuid)
limitqueryintegerRows per page (1–1000, default 100).
offsetqueryintegerRows 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.

FieldInTypeDescription
actionquery"paused" | "scaled" | "duplicated" | "alerted" | "all"
statusquery"success" | "failure" | "all"
campaign_searchquerystring
date_rangequery"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_datequerystring (date)YYYY-MM-DD, use with end_date.
end_datequerystring (date)YYYY-MM-DD (inclusive).
limitqueryintegerRows (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.

FieldInTypeDescription
account_idquerystringFacebook ad account id, e.g. act_1234567890.
activequery"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).

FieldInTypeDescription
fb_account_idrequiredbodystring
namerequiredbodystring
descriptionbodystring | null
conditionsrequiredbodyobject[]
data_intervalbody"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_modebody"simple" | "ai_headlines" | "ai_scripts" | "ai_full"
budget_overridebodynumber | null
page_overridebodystring | null
start_immediatelybodyboolean
copy_automation_rulesbodyboolean
name_suffixbodystring
ai_headline_countbodyinteger | null
ai_script_countbodyinteger | null
ai_personabodystring | null
ai_tonebodystring | null
ai_providerbody"anthropic" | "openai" | null
ai_modelbodystring | null
ai_headline_prompt_versionbodystring | null
ai_script_prompt_versionbodystring | null
max_clones_per_daybodyinteger
min_hours_between_clonesbodyinteger
post_failure_cooldown_hoursbodyinteger | null
apply_tobody"all" | "campaign"
campaign_idsbodystring[] | null
scheduling_modebody"trigger" | "scheduled"
scheduled_atbodystring (date-time) | null
recurring_patternbody"daily" | "weekly" | "monthly" | null
recurring_hourbodyinteger | null
recurring_daybodyinteger | null
schedule_timezonebodystring | null
next_scheduled_runbodystring (date-time) | null
active_hours_startbodyinteger | null
active_hours_endbodyinteger | null
is_activebodyboolean
created_bybodystringFree-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+

One clone rule.

FieldInTypeDescription
rule_idrequiredpathstring (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.

FieldInTypeDescription
rule_idrequiredpathstring (uuid)
namebodystring
descriptionbodystring | null
conditionsbodyobject[]
data_intervalbody"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_modebody"simple" | "ai_headlines" | "ai_scripts" | "ai_full"
budget_overridebodynumber | null
page_overridebodystring | null
start_immediatelybodyboolean
copy_automation_rulesbodyboolean
name_suffixbodystring
ai_headline_countbodyinteger | null
ai_script_countbodyinteger | null
ai_personabodystring | null
ai_tonebodystring | null
ai_providerbody"anthropic" | "openai" | null
ai_modelbodystring | null
ai_headline_prompt_versionbodystring | null
ai_script_prompt_versionbodystring | null
max_clones_per_daybodyinteger
min_hours_between_clonesbodyinteger
post_failure_cooldown_hoursbodyinteger | null
apply_tobody"all" | "campaign"
campaign_idsbodystring[] | null
scheduling_modebody"trigger" | "scheduled"
scheduled_atbodystring (date-time) | null
recurring_patternbody"daily" | "weekly" | "monthly" | null
recurring_hourbodyinteger | null
recurring_daybodyinteger | null
schedule_timezonebodystring | null
next_scheduled_runbodystring (date-time) | null
active_hours_startbodyinteger | null
active_hours_endbodyinteger | null
is_activebodyboolean
created_bybodystringFree-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.

FieldInTypeDescription
rule_idrequiredpathstring (uuid)
confirmrequiredquery"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).

FieldInTypeDescription
rule_idrequiredpathstring (uuid)
activerequiredbodybooleantrue = rule runs; false = paused.
created_bybodystringFree-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.

FieldInTypeDescription
rule_idrequiredpathstring (uuid)
limitqueryintegerRows per page (1–1000, default 100).
offsetqueryintegerRows 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.

FieldInTypeDescription
account_idquerystringFacebook 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.

FieldInTypeDescription
namerequiredbodystring
is_enabledbodyboolean
fb_account_idrequiredbodystring
fb_page_idbodystring | null
trigger_metricrequiredbody"likes_24h_delta" | "comments_24h_delta" | "shares_24h_delta" | "engagement_24h_delta" | "engagement_72h_delta" | "total_engagement"
trigger_thresholdrequiredbodyinteger
template_adset_fb_idrequiredbodystring
daily_budget_majorrequiredbodynumber
objectivebodystring
cooldown_per_post_hoursbodyinteger
max_boosts_per_daybodyinteger
created_bybodystringFree-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+

One auto-boost rule.

FieldInTypeDescription
rule_idrequiredpathstring (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.

FieldInTypeDescription
rule_idrequiredpathstring (uuid)
namebodystring
is_enabledbodyboolean
fb_page_idbodystring | null
trigger_metricbody"likes_24h_delta" | "comments_24h_delta" | "shares_24h_delta" | "engagement_24h_delta" | "engagement_72h_delta" | "total_engagement"
trigger_thresholdbodyinteger
template_adset_fb_idbodystring
daily_budget_majorbodynumber
objectivebodystring
cooldown_per_post_hoursbodyinteger
max_boosts_per_daybodyinteger
created_bybodystringFree-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.

FieldInTypeDescription
rule_idrequiredpathstring (uuid)
confirmrequiredquery"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.

FieldInTypeDescription
rule_idrequiredpathstring (uuid)
activerequiredbodybooleantrue = rule runs; false = paused.
created_bybodystringFree-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.

FieldInTypeDescription
rule_idrequiredpathstring (uuid)
limitqueryintegerRows per page (1–1000, default 100).
offsetqueryintegerRows 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"
    }
  ]
}

Conversions

GET/v1/conversions

List conversions

Scope: read:conversionsRole: viewer+

Every recorded conversion (from postbacks, the pixel, Shopify, and this API), newest first, with how it was attributed. Account-restricted keys only see conversions attributed to their ad accounts.

FieldInTypeDescription
page_idquerystringOnly conversions from this page test.
campaign_idquerystringNumeric Facebook id (campaign / ad set / ad).
attributedquery"true" | "false"true = attributed to a Facebook ad; false = unattributed.
date_rangequery"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_datequerystring (date)YYYY-MM-DD (inclusive). Use with end_date.
end_datequerystring (date)YYYY-MM-DD (inclusive). Max range 548 days. Ignored when date_range is set.
limitqueryintegerRows per page (1–1000, default 100).
offsetqueryintegerRows to skip.

Request

curl -X GET "https://api.roas.to/v1/conversions" \
  -H "Authorization: Bearer $ROAS_API_KEY"

Response 200

{
  "total": 1,
  "limit": 100,
  "offset": 0,
  "has_more": false,
  "conversions": [
    {
      "id": "3b4c5d6e-7f8a-4b9c-8d0e-1f2a3b4c5d6e",
      "timestamp": "2026-10-19T08:42:11.000-04:00",
      "revenue": 49,
      "currency": "USD",
      "transaction_id": "ord_1001",
      "order_id": "1001",
      "attribution_method": "sub4_session",
      "page_id": "keto-advertorial",
      "variant_id": "v2",
      "fb_campaign_id": "120210000000000001",
      "fb_adset_id": "120210000000000002",
      "fb_ad_id": "120210000000000003"
    }
  ]
}
POST/v1/conversions

Record a conversion (server-side)

Scope: write:conversionsRole: editor+Idempotency-Key

Sends a sale from your server. It runs through the same pipeline as tracker postbacks: attribution (sub4 first, then sub1/2/3), cross-source dedup on order_id, the boosted-post guard, and Meta Conversions API delivery. Idempotent on order_id / transaction_id.

FieldInTypeDescription
revenuerequiredbodynumberSale amount (MAJOR units).
currencybodystringISO 4217 code. Defaults to the workspace currency.
order_idbodystringYour order id (dedupes with Shopify).
transaction_idbodystringUnique sale id.
sub4bodystringThe sub4 click id the visitor arrived with (the deterministic attribution bond).
sub1bodystringCampaign id from the click URL.
sub2bodystringAd set id from the click URL.
sub3bodystringAd id from the click URL.
product_idbodystring
created_bybodystringFree-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/conversions" \
  -H "Authorization: Bearer $ROAS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "revenue": 50
  }'

Response 201

{
  "recorded": true
}
GET/v1/tracker/offers

Tracker offers

Scope: read:conversionsRole: viewer+

Offers, landing pages, and smart links from your connected tracker (Everflow), with status and URLs.

FieldInTypeDescription
searchquerystring
statusquery"active" | "paused" | "expired" | "all"
limitqueryintegerRows (1–50, default 20).

Request

curl -X GET "https://api.roas.to/v1/tracker/offers" \
  -H "Authorization: Bearer $ROAS_API_KEY"

Response 200

{
  "offers": [
    {
      "id": "2a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6d",
      "external_offer_id": "12",
      "name": "Keto Gummies — US",
      "status": "active",
      "payout": 45,
      "payout_type": "cpa",
      "urls": []
    }
  ]
}

Approvals

GET/v1/approvals

List approval requests

Scope: read:automationRole: viewer+

Writes that a workspace approval rule queued (the API returned 202 + pending_action_id). Owners and admins see the whole workspace; other roles see only their own requests. Statuses: pending → approved → executed / failed, or rejected / expired. Approving happens in ROAS.to (Team → Approvals). A human gate can't be approved by the key it gates.

FieldInTypeDescription
statusquery"pending" | "approved" | "rejected" | "expired" | "executed" | "failed"
limitqueryintegerRows per page (1–1000, default 100).
offsetqueryintegerRows to skip.

Request

curl -X GET "https://api.roas.to/v1/approvals" \
  -H "Authorization: Bearer $ROAS_API_KEY"

Response 200

{
  "total": 1,
  "limit": 100,
  "offset": 0,
  "has_more": false,
  "approvals": [
    {
      "id": "5d0c3a52-6b1f-4c1e-9d8f-2a6b7f0c9e11",
      "action_type": "budget_increase",
      "target_summary": "Set daily budget on 1 campaign(s) (API)",
      "status": "pending",
      "requested_by_user_id": "user_2abc",
      "requested_by_you": true,
      "approver_user_id": null,
      "decision_at": null,
      "decision_note": null,
      "executed_at": null,
      "execution_error": null,
      "expires_at": "2026-10-20T09:00:00.000-04:00",
      "created_at": "2026-10-19T09:00:00.000-04:00"
    }
  ]
}
GET/v1/approvals/{approval_id}

Get an approval request

Scope: read:automationRole: viewer+

Poll this after a 202 to learn whether the write was approved and executed.

FieldInTypeDescription
approval_idrequiredpathstring (uuid)

Request

curl -X GET "https://api.roas.to/v1/approvals/{approval_id}" \
  -H "Authorization: Bearer $ROAS_API_KEY"

Response 200

{
  "id": "5d0c3a52-6b1f-4c1e-9d8f-2a6b7f0c9e11",
  "action_type": "budget_increase",
  "target_summary": "Set daily budget on 1 campaign(s) (API)",
  "status": "pending",
  "requested_by_user_id": "user_2abc",
  "requested_by_you": true,
  "approver_user_id": null,
  "decision_at": null,
  "decision_note": null,
  "executed_at": null,
  "execution_error": null,
  "expires_at": "2026-10-20T09:00:00.000-04:00",
  "created_at": "2026-10-19T09:00:00.000-04:00"
}
POST/v1/approvals/{approval_id}/cancel

Withdraw your pending request

Scope: write:status or write:budgets or write:bids or write:clone or write:create or write:delete or write:automationRole: viewer+Idempotency-Key

Withdraws a still-pending request made by this key’s creator.

FieldInTypeDescription
approval_idrequiredpathstring (uuid)
created_bybodystringFree-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/approvals/{approval_id}/cancel" \
  -H "Authorization: Bearer $ROAS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{}'

Response 200

{
  "status": "completed",
  "approval_status": "rejected"
}

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.

FieldInTypeDescription
urlrequiredbodystring (uri)
eventsrequiredbody"*" | "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.
descriptionbodystring
created_bybodystringFree-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.

FieldInTypeDescription
endpoint_idrequiredpathstring (uuid)
is_activebodyboolean
eventsbody"*" | "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.
descriptionbodystring | null
created_bybodystringFree-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.

FieldInTypeDescription
endpoint_idrequiredpathstring (uuid)
confirmrequiredquery"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.

FieldInTypeDescription
endpoint_idrequiredpathstring (uuid)
created_bybodystringFree-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.

FieldInTypeDescription
endpoint_idrequiredpathstring (uuid)
created_bybodystringFree-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.

FieldInTypeDescription
endpoint_idquerystring (uuid)
statusquery"pending" | "delivered" | "failed"
event_typequerystring
limitqueryintegerRows per page (1–1000, default 100).
offsetqueryintegerRows 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.

FieldInTypeDescription
limitqueryintegerRows per page (1–100).
offsetqueryintegerRows 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.

FieldInTypeDescription
page_idrequiredbodystring
page_namerequiredbodystring
original_urlrequiredbodystring (uri)
product_idbodystring | null
traffic_source_idbodystring | null
direct_affiliate_linkbodystring (uri) | null
modificationsrequiredbodyobject[]
trackingrequiredbodyobject
detected_elementsbodyobject
created_bybodystringFree-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).

FieldInTypeDescription
page_idrequiredpathstringPage 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.

FieldInTypeDescription
page_idrequiredpathstringPage test id (the page_id slug).
modificationsbodyobject[]
notesbodystring
created_bybodystringFree-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.

FieldInTypeDescription
page_idrequiredpathstringPage test id (the page_id slug).
confirmrequiredquery"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.

FieldInTypeDescription
page_idrequiredpathstringPage 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.

FieldInTypeDescription
page_idrequiredpathstringPage 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).
FieldInTypeDescription
page_idrequiredpathstringPage test id (the page_id slug).
split_modebody"auto" | "manual"
baseline_traffic_pctbodynumber
target_test_countbodyinteger
auto_generation_enabledbodyboolean
auto_promote_enabledbodyboolean
auto_pause_enabledbodyboolean
auto_cull_enabledbodyboolean
holdout_enabledbodyboolean
holdout_percentagebodyintegerHoldout share in basis points (500 = 5%).
min_views_for_promotionbodyinteger | null
min_views_for_evalbodyinteger | null
weak_loser_viewsbodyinteger | null
weak_loser_thresholdbodynumber | null
promotion_confidencebodynumber | null
pause_thresholdbodynumber | null
created_bybodystringFree-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).

FieldInTypeDescription
page_idrequiredpathstringPage test id (the page_id slug).
modebody"redirect" | "passthrough"
link_targetingbodyobject
direct_affiliate_linkbodystring (uri) | nullReplace-destination URL. Omit to keep the current one; null clears it.
stamp_timingbody"render" | "click"
created_bybodystringFree-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).

FieldInTypeDescription
page_idrequiredpathstringPage 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.

FieldInTypeDescription
page_idrequiredpathstringPage test id (the page_id slug).
created_bybodystringFree-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.

FieldInTypeDescription
page_idrequiredpathstringPage test id (the page_id slug).
created_bybodystringFree-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.
FieldInTypeDescription
page_idrequiredpathstringPage test id (the page_id slug).
moderequiredbody"auto" | "manual" | "even"
allocationsbodyobject[]
created_bybodystringFree-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.

FieldInTypeDescription
page_idrequiredpathstringPage test id (the page_id slug).
created_bybodystringFree-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.

FieldInTypeDescription
page_idrequiredpathstringPage test id (the page_id slug).
created_bybodystringFree-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.

FieldInTypeDescription
page_idrequiredpathstringPage 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.

FieldInTypeDescription
page_idrequiredpathstringPage test id (the page_id slug).
variant_idbodystring
modificationsbodyobject[]
changes_descriptionbodystring
hypothesisbodystring
notesbodystring
created_bybodystringFree-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.

FieldInTypeDescription
page_idrequiredpathstringPage test id (the page_id slug).
variant_idrequiredpathstringVariant 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.

FieldInTypeDescription
page_idrequiredpathstringPage test id (the page_id slug).
variant_idrequiredpathstringVariant id within a test.
statusbody"active" | "paused"
modificationsbodyobject[]
hypothesisbodystring
notesbodystring
created_bybodystringFree-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.

FieldInTypeDescription
page_idrequiredpathstringPage test id (the page_id slug).
variant_idrequiredpathstringVariant 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.

FieldInTypeDescription
page_idrequiredpathstringPage test id (the page_id slug).
variant_idrequiredpathstringVariant id within a test.
created_bybodystringFree-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"
}