roasOS
Workflows

Extract a brand

Read one brand from its own website: the products, the offer, the promise, the proof and the voice.

Extract a brand reads a website and returns what the brand says about itself. Use the result to fill your brand kit, and to compile the intelligence documents that the agent reads.

An extraction runs as a job with its own read path. Quote the run, confirm the run, poll the status, then read the result.

1. Send the website

The request has one field.

FieldRule
website_urla public https URL, 2,048 characters at most

roasOS refuses a URL that is not https, a URL that carries credentials, a URL with a custom port and a URL that points at an internal host. Each refusal is 400 validation_error.

Every crawl bound is fixed by roasOS. The request cannot widen one, because a wider crawl is a higher price.

2. Quote

Set quote_only to true. A quote is free.

curl -X POST https://api.roasos.com/v1/workflows/extract-brand \
  -H "Authorization: Bearer roasos_sk_..." \
  -H "Idempotency-Key: extract-acme-001" \
  -H "Content-Type: application/json" \
  -d '{
    "website_url": "https://www.acme.com",
    "quote_only": true
  }'
{
  "data": {
    "schema_version": "workflow-analysis@1",
    "kind": "workflow_analysis",
    "id": "3c1f8ab2-...",
    "workflow": "extract-brand",
    "status": "quoted",
    "analysis_kind": "website",
    "cache_disposition": "live_analysis",
    "reserved_credits": 0,
    "maximum_credits": 2,
    "pricing_version": "workflow-pricing@2026-10-01.v2",
    "quote_id": "9d40b7c5-...",
    "expires_at": "2026-08-21T10:17:44.000Z",
    "authorization": { "status": "auto_approved" },
    "created_at": "2026-08-21T10:07:44.000Z",
    "updated_at": "2026-08-21T10:07:44.000Z"
  }
}

An extraction is flat at 2 credits. The price does not change with the size of the site.

3. Confirm

Send the same body with the quote_id. Use the same idempotency key.

curl -X POST https://api.roasos.com/v1/workflows/extract-brand \
  -H "Authorization: Bearer roasos_sk_..." \
  -H "Idempotency-Key: extract-acme-001" \
  -H "Content-Type: application/json" \
  -d '{
    "website_url": "https://www.acme.com",
    "quote_id": "9d40b7c5-..."
  }'

The answer is 202. The credits are now reserved.

{
  "data": {
    "schema_version": "workflow-analysis@1",
    "kind": "workflow_analysis",
    "id": "3c1f8ab2-...",
    "workflow": "extract-brand",
    "status": "queued",
    "analysis_kind": "website",
    "cache_disposition": "live_analysis",
    "reserved_credits": 2,
    "maximum_credits": 2,
    "pricing_version": "workflow-pricing@2026-10-01.v2",
    "created_at": "2026-08-21T10:07:44.000Z",
    "updated_at": "2026-08-21T10:08:02.000Z"
  }
}

Idempotency-Key is required on both calls. Use 16 to 255 characters. roasOS answers 400 idempotency_key_required without it.

4. Poll the status

An extraction sends no webhook. Poll the status every few seconds.

curl https://api.roasos.com/v1/workflows/analyses/3c1f8ab2-.../status \
  -H "Authorization: Bearer roasos_sk_..."

The status changes from queued to processing. It then becomes succeeded or failed. A succeeded run carries result_ready: true.

An analysis uses its own status words. The terminal success is succeeded, and it is never completed. A generation carries the five job statuses instead, and its terminal success is completed. Refer to Jobs.

An extraction is one analysis run, so it uses the read path of every analysis. GET /v1/workflows/analyses/{id} returns the same envelope as the status path. An extraction adds no second read family, and it adds no third status vocabulary.

5. Read the result

curl https://api.roasos.com/v1/workflows/analyses/3c1f8ab2-.../result \
  -H "Authorization: Bearer roasos_sk_..."

/result returns 409 analysis_not_completed while the run is not terminal. This is not an error in your code. Poll /status first.

{
  "data": {
    "schema_version": "workflow-analysis-result@1",
    "kind": "workflow_analysis_result",
    "analysis_id": "3c1f8ab2-...",
    "workflow": "extract-brand",
    "status": "succeeded",
    "analysis_kind": "website",
    "cache_disposition": "live_analysis",
    "model": "gemini-3-flash-preview",
    "pricing_version": "workflow-pricing@2026-10-01.v2",
    "reserved_credits": 2,
    "maximum_credits": 2,
    "settled_credits": 2,
    "completed_at": "2026-08-21T10:09:12.000Z",
    "analysis": {
      "schemaVersion": "site-analysis.v1",
      "websiteUrl": "https://www.acme.com",
      "observedAt": "2026-08-21T10:08:40.000Z",
      "corpus": {
        "sha256": "9f2c...",
        "byteLength": 48210,
        "pageCount": 9,
        "truncated": false,
        "deadlineReached": false,
        "robotsRestricted": false,
        "homepageSource": "browser-rendering",
        "reviewsRead": true,
        "verifiedDowngraded": false,
        "pages": [
          { "url": "https://www.acme.com/", "bucket": "home", "title": "Acme", "byteLength": 8102 }
        ]
      },
      "extraction": {
        "brandName": "Acme",
        "category": "skincare for sensitive skin",
        "voice": "Direct and warm. Short sentences. No hype.",
        "product": "A fragrance-free barrier cream in a 50 ml jar.",
        "mechanism": "Ceramides rebuild the skin barrier over eight weeks.",
        "differentiation": "One ingredient list, published in full.",
        "offer": "Free shipping over 40 euro. Sixty-day return.",
        "desires": "Calm skin without a ten-step routine.",
        "sophisticationStage": 3,
        "newAngle": "Name the price of the routine the customer already runs.",
        "avatars": [
          {
            "name": "Pharmacy buyer",
            "who": "Women 25 to 40 who buy skincare in a pharmacy.",
            "pains": "Redness after every new product.",
            "desires": "One product that does not sting.",
            "objections": "It is too expensive for a cream.",
            "evidence": "I stopped buying the expensive one."
          }
        ],
        "proof": "Dermatologist tested. 1,204 reviews at 4.6.",
        "competitors": [
          { "name": "Beta Skin", "url": "https://betaskin.com", "note": "Same price, louder claims." }
        ],
        "confidence": {
          "voice": "stated",
          "product": "stated",
          "mechanism": "stated",
          "differentiation": "inferred",
          "offer": "stated",
          "desires": "verified",
          "newAngle": "inferred",
          "proof": "verified"
        },
        "gaps": ["Which skin types is the cream not for?"]
      },
      "candidate": {
        "schemaVersion": "brand_analysis_candidate_v1",
        "status": "unconfirmed",
        "sourceUrl": "https://www.acme.com",
        "fields": {}
      },
      "logoMediaKey": "uploads/8f1c.../images/logo-light.png"
    }
  }
}
PartWhat it holds
corpuswhich pages roasOS read, and how the crawl ended
extractionthe brand: the voice, the product, the mechanism, the offer, the avatars, the proof, the competitors and the gaps
candidatethe same brand fields as an unconfirmed candidate, ready for a person to confirm
logoMediaKeythe logo of the site, mirrored into your own workspace storage, or null

competitors is model-asserted. roasOS did not verify a competitor, and it did not verify the url of one.

Per-field confidence

extraction.confidence carries one label for each of eight fields: voice, product, mechanism, differentiation, offer, desires, newAngle and proof.

LabelMeaning
statedthe brand states this about itself on its own site
inferredthe model concluded this from the pages it read
verifieda reviews page supports this

Only a reviews page earns verified. Read corpus.reviewsRead to see whether roasOS read one. corpus.verifiedDowngraded is true when roasOS read no reviews page and moved a verified label down to stated.

Treat inferred as a proposal. Confirm it before you build a campaign on it.

The crawl bounds

roasOS reads the homepage first, then reads the pages that carry brand signal.

BoundValue
Pages12 at most, the homepage included
Whole crawl60 seconds; roasOS then uses the pages it holds
One page8 seconds and 750 KB at most
Redirects5 at most, and the destination must stay on the same origin
robots.txtroasOS obeys each Disallow rule that names roasOS or every agent

A page with less than 80 characters of text carries no signal, so roasOS drops it from the corpus.

roasOS reads the homepage through a browser, so a page that draws itself with JavaScript still gives text. corpus.homepageSource says which reader ran: browser-rendering is the normal path, and crawl says that the browser was not available and a plain fetch read the homepage instead.

Three fields of corpus report how the crawl ended.

Fieldtrue means
deadlineReachedthe 60-second deadline stopped the crawl, and roasOS used the pages it held
robotsRestrictedrobots.txt refused one or more pages
truncatedthe corpus was longer than the model ceiling, so roasOS cut it

The cache

roasOS keeps one extraction of one site for your workspace for seven days.

  • A second request inside those seven days is a cache hit. It costs 0 credits. The answer is succeeded at once, with cache_disposition: cache_hit. Read the result immediately.
  • A request after those seven days is a new crawl at the full price.

The cache belongs to your workspace. It is not shared with another workspace. There is no field that forces a new crawl of an unchanged site.

What the extraction feeds

  • The logo is mirrored into your workspace storage under uploads/. Put its URL into logo_url on PUT /v1/brand-kit. The kit then records the extraction in source_analysis_id.
  • The brand fields become candidates. Confirm a field in the roasOS app. Refer to Intelligence.
  • The result feeds the compiled documents. Call POST /v1/intelligence/refresh to compile them now. A refresh is free.

Failure and refunds

A failed run carries settled_credits: 0. roasOS refunds the reservation in full.

CodeCause
analysis_failedthe model call did not produce a usable document
analysis_media_unreadableroasOS could not read the site
analysis_timed_outthe run passed its time limit

Rate window

The extraction runs on the included lane of roasOS. That lane holds its own hourly window for each workspace: 100 extractions per hour. A request over the window answers 429 included_ancillary_ai_window_exceeded and carries a Retry-After header. Wait for the seconds in that header. Then send the request again with the same idempotency key.

A cache hit never reaches the lane, so it never meets this window.

Other errors

CodeStatusCause
validation_error400website_url breaks a rule above
idempotency_key_required400the Idempotency-Key header is absent
quote_expired410the quote expired, or it does not match this body; quote again with a new idempotency key
analysis_not_completed409you read /result before the run was terminal
analysis_unavailable503the run could not be read; send the request again

From an agent

roasos_extract_brand starts an extraction from an MCP client. Read the run with roasos_get_ad_analysis, roasos_get_ad_analysis_status and roasos_get_ad_analysis_result, because an extraction is one analysis run. The quote rule is the same. Refer to Connect an AI agent (MCP).

Next

On this page