roasOS
Workflows

Research competitor ads

Find the ads that a company runs on Facebook, in one normalized shape.

Research finds the ads that a company runs on Facebook. Use the records for competitor analysis. Use them also as evidence for your next creative.

Research is a paid job with its own read paths. Quote it, confirm it, poll the status, then read the result. A live fetch costs 1 credit. A fresh cache hit costs 0 credits.

Two research capabilities are live:

  • research.facebook-company-ads@1 reads one page of up to 30 ads for one Facebook Page ID.
  • research.facebook-ad-search@1 reads one page of up to 30 ads for one keyword.

roasOS declares seven more capabilities and blocks each one. A request that names a blocked capability answers 400 validation_error.

Research uses its own status words

A research job carries seven statuses:

quoted, waiting_cache, queued, processing, succeeded, failed, unknown.

Two of them differ from the job vocabulary. Read the five published job statuses on Jobs, then note these two rules:

  • The terminal success of a research job is succeeded. It is never completed.
  • unknown belongs to research alone. It says that roasOS cannot yet state the outcome of the credit. The envelope carries reconciliation_required: true. roasOS settles the reservation itself, and the job then reports its real terminal status. Do not send the request again.

A research job has its own resource, so it is absent from the job list. GET /v1/jobs?kind=research answers an empty page. Read a research job on GET /v1/research/jobs/{id}.

1. Quote

Name the Facebook Page ID of the company and your filters. Set quote_only to true to read the price. A quote is free.

Send an Idempotency-Key header on the quote and on the confirm. Use the same value for both.

curl -X POST https://api.roasos.com/v1/research/jobs \
  -H "Authorization: Bearer roasos_sk_..." \
  -H "Idempotency-Key: nike-research-001" \
  -H "Content-Type: application/json" \
  -d '{
    "capability_id": "research.facebook-company-ads@1",
    "target": { "kind": "page_id", "page_id": "15087023444" },
    "filters": { "country": "ALL", "status": "ACTIVE", "media_type": "ALL", "sort_by": "relevancy_monthly_grouped" },
    "page": { "max_pages": 1, "max_results": 30 },
    "freshness": { "max_age_seconds": 21600, "allow_stale": true },
    "quote_only": true
  }'
{
  "data": {
    "schema_version": "research-quote@1",
    "kind": "research_quote",
    "status": "quoted",
    "quote_id": "dddddddd-...",
    "capability_id": "research.facebook-company-ads@1",
    "cache_disposition": "live_fetch",
    "reserved_credits": 0,
    "maximum_credits": 1,
    "expires_at": "2026-08-22T12:10:00.000Z",
    "authorization": { "status": "auto_approved" },
    "confirmation": {
      "max_pages": 1,
      "max_results": 30,
      "max_age_seconds": 21600,
      "allow_stale": true
    }
  }
}

Read two fields before you confirm:

  • maximum_credits is the ceiling of the run. It is 1 on a live fetch and 0 on a fresh cache hit.
  • cache_disposition says which one you get. Its values are cache_hit, live_fetch and cache_join.

The envelope also carries a pricing_version. Research prices on its own version string, and not on the model pricing version. Refer to Quotes and credits.

The request fields

FieldRule
capability_idresearch.facebook-company-ads@1
target.kindpage_id
target.page_idThe numeric Facebook Page ID of the company
filters.countryAn ISO country code, or ALL
filters.statusACTIVE
filters.media_typeALL
filters.languageTwo capital letters, for example EN
filters.sort_byrelevancy_monthly_grouped
filters.start_date, filters.end_dateYYYY-MM-DD. The end date must not precede the start date
page.max_pages1
page.max_results1 to 30
freshness.max_age_seconds0 to 21600, which is six hours
freshness.allow_staletrue or false

Every filter field is optional. The body needs capability_id, target, filters, page and freshness. It takes no other field.

2. Confirm

Send the same body with the quote_id in place of quote_only. Use the same idempotency key.

This step reserves your credits.

curl -X POST https://api.roasos.com/v1/research/jobs \
  -H "Authorization: Bearer roasos_sk_..." \
  -H "Idempotency-Key: nike-research-001" \
  -H "Content-Type: application/json" \
  -d '{
    "capability_id": "research.facebook-company-ads@1",
    "target": { "kind": "page_id", "page_id": "15087023444" },
    "filters": { "country": "ALL", "status": "ACTIVE", "media_type": "ALL", "sort_by": "relevancy_monthly_grouped" },
    "page": { "max_pages": 1, "max_results": 30 },
    "freshness": { "max_age_seconds": 21600, "allow_stale": true },
    "quote_id": "dddddddd-..."
  }'
{
  "data": {
    "schema_version": "research-job@1",
    "kind": "research_job",
    "id": "eeeeeeee-...",
    "capability_id": "research.facebook-company-ads@1",
    "status": "queued",
    "cache_disposition": "live_fetch",
    "reserved_credits": 1,
    "maximum_credits": 1,
    "created_at": "2026-08-22T12:00:00.000Z",
    "updated_at": "2026-08-22T12:00:00.000Z"
  }
}

quote_only: true and quote_id together answer 400. Send one of the two.

If the price is above the approval threshold of your key, the quote answers authorization.status: approval_required with an authorize_url. The response also carries the header X-RoasOS-Approval-Required: true. A confirm without that approval answers 403 spend_approval_required.

3. Poll the status

Research sends no webhook. Poll the status every few seconds.

curl https://api.roasos.com/v1/research/jobs/eeeeeeee-.../status \
  -H "Authorization: Bearer roasos_sk_..."

The status moves from queued to processing. It then becomes succeeded, failed or unknown. A succeeded job carries result_ready: true and completed_at.

A job that waits on another caller's identical fetch reports waiting_cache with cache_disposition: cache_join. It reserves nothing.

GET /v1/research/jobs/{id} returns the same envelope. Use /status for the poll loop.

4. Read the result

curl https://api.roasos.com/v1/research/jobs/eeeeeeee-.../result \
  -H "Authorization: Bearer roasos_sk_..."

/result answers 409 research_result_not_ready while the status is not succeeded. This is not a defect in your code. Poll /status first.

{
  "data": {
    "schema_version": "research-result@1",
    "kind": "research_result",
    "job_id": "eeeeeeee-...",
    "capability_id": "research.facebook-company-ads@1",
    "status": "succeeded",
    "cache_disposition": "live_fetch",
    "reserved_credits": 1,
    "maximum_credits": 1,
    "settled_credits": 1,
    "provenance": {
      "source": "facebook",
      "native_id": "15087023444",
      "fetched_at": "2026-08-22T11:00:00.000Z",
      "fresh_through": "2026-08-22T17:00:00.000Z"
    },
    "result": {
      "kind": "facebook_company_ads",
      "item_count": 1,
      "items": [
        {
          "id": "1212560900568826",
          "source": "facebook_ad",
          "advertiser": { "id": "15087023444", "name": "Nike", "avatar_url": "https://cdn.example.com/nike.jpg" },
          "media": {
            "type": "image",
            "slides": [{ "type": "image", "thumbnail_url": "https://cdn.example.com/ad.jpg", "video_url": null }]
          },
          "copy": "Winter creative",
          "title": "Look activated",
          "description": "Public description",
          "domain": "nike.com",
          "cta": "Shop now",
          "destination_url": "https://www.nike.com/winter",
          "run_start_date": "2025-06-17",
          "run_end_date": null,
          "is_active": true,
          "platform_link": "https://www.facebook.com/ads/library?id=1212560900568826"
        }
      ]
    }
  }
}

Each record keeps the observed data separate from its source. provenance gives the source, the native id, the fetch time and the freshness horizon. The result carries no raw provider payload and no billing record.

Cache and freshness

roasOS holds one cache entry for each identical request.

  • max_age_seconds sets the oldest data you accept. The ceiling is 21600 seconds. A cache entry inside that age is a fresh hit, and roasOS returns it at once for 0 credits.
  • allow_stale permits older cached data. Set it to false to force a live fetch.
  • Two identical misses at the same time join one paid fetch. The second job reports cache_join and reserves nothing.

When roasOS charges nothing

Research reserves at most 1 credit, and it settles only a useful live fetch.

Casecache_dispositionCredits
A fresh cache hitcache_hit0
A join onto another live fetchcache_join0
A live fetch that returns adslive_fetch1
A live fetch that returns no adslive_fetch0
A failed joblive_fetch0

A job that costs you nothing carries settled_credits: 0. roasOS refunds the reservation in full.

An empty live fetch and a failed live fetch both hold a reservation for a short time. The status is unknown while roasOS returns the credit. It becomes succeeded or failed when the refund lands.

Errors

CodeCause
400 validation_errorThe body broke a field rule of the table above
400 research_quote_requiredThe confirm carried no quote_id
402 insufficient_creditsThe workspace balance is below the ceiling
403 spend_approval_requiredThe price is above the approval threshold of the key
404 research_job_not_foundThe id belongs to another workspace, or to another key
409 research_result_not_readyThe status is not succeeded yet
503 research_unavailableroasOS cannot read the job or the result now
503 research_quote_store_unavailableroasOS cannot issue a quote now

A failed job also carries an error code of its own: research_failed or normalization_failed. Refer to Errors.

Search the Meta Ad Library by keyword

The keyword search reads live ads by words, and not by company. Use it when you do not know the Page ID, or when you want the ads of a whole category.

The steps are the same: quote, confirm, poll, then read the result. A live fetch costs 1 credit. A fresh cache hit costs 0 credits.

curl -X POST https://api.roasos.com/v1/research/jobs \
  -H "Authorization: Bearer roasos_sk_..." \
  -H "Idempotency-Key: betting-search-001" \
  -H "Content-Type: application/json" \
  -d '{
    "capability_id": "research.facebook-ad-search@1",
    "target": { "kind": "query", "query": "sports betting app" },
    "filters": { "country": "US", "media_type": "VIDEO", "sort_by": "total_impressions" },
    "page": { "max_pages": 1, "max_results": 12 },
    "freshness": { "max_age_seconds": 21600, "allow_stale": false },
    "quote_only": true
  }'

Confirm with the same body. Replace quote_only with the quote_id. Send the same idempotency key.

The request fields

FieldRule
capability_idresearch.facebook-ad-search@1
target.kindquery
target.query2 to 200 characters. One line, single spaces, no leading or trailing space, no control character
filters.countryAn ISO country code, or ALL
filters.statusACTIVE
filters.media_typeALL, IMAGE or VIDEO
filters.languageTwo capital letters, for example EN
filters.sort_bytotal_impressions or relevancy_monthly_grouped
filters.start_date, filters.end_dateYYYY-MM-DD. The end date must not precede the start date
page.max_pages1
page.max_results1 to 30
freshness.max_age_seconds0 to 21600, which is six hours
freshness.allow_staletrue or false

Every filter field is optional. roasOS fills the defaults country: ALL, status: ACTIVE, media_type: ALL and sort_by: total_impressions. The filled body is the cache identity, so an omitted default and an explicit default name the same cache row.

The result

The result carries result.kind: "facebook_ad_search". Each item holds the same normalized ad shape the company capability returns. The ads come from many advertisers, so provenance.native_id holds your query instead of a Page ID.

{
  "data": {
    "capability_id": "research.facebook-ad-search@1",
    "status": "succeeded",
    "provenance": {
      "source": "facebook",
      "native_id": "sports betting app",
      "fetched_at": "2026-09-05T11:00:00.000Z",
      "fresh_through": "2026-09-05T17:00:00.000Z"
    },
    "result": { "kind": "facebook_ad_search", "item_count": 12, "items": ["..."] }
  }
}

From an agent

Four MCP tools cover research. The quote rule is the same, and the paid tool never approves spend for you.

ToolPathMark
roasos_run_researchPOST /v1/research/jobspaid
roasos_get_research_jobGET /v1/research/jobs/{id}read
roasos_get_research_statusGET /v1/research/jobs/{id}/statusread
roasos_get_research_resultGET /v1/research/jobs/{id}/resultread

Refer to Connect an AI agent (MCP).

Next

On this page