---
title: Quickstart
description: "Create an account via Stripe checkout, add your brand, run the pipeline, and see ranked content opportunities — from your terminal in under 10 minutes."
---

> **For AI agents:** the complete documentation index is at [llms.txt](https://docs.boringmarketing.com/llms.txt). Append `.md` to any page URL for its markdown version.

Five steps. No decisions. Copy each command, paste it, move to the next.

By the end you will have a registered brand with ranked content opportunities telling you exactly what to write and why.

<Tip>
  **Human setting this up in a browser?** Skip the checkout curl and start at [dashboard.boringmarketing.com](https://dashboard.boringmarketing.com). Same Stripe flow, same API key — just with a UI.
</Tip>

<Steps>
  <Step title="Create your account via Stripe checkout">
    Call `/auth/checkout` with your email and desired tier. The server emails you a Stripe checkout link. Pay, then retrieve your API key from the dashboard.

    <CodeGroup>
    ```bash cURL
    curl -s -X POST \
      https://api.boringmarketing.com/auth/checkout \
      -H "Content-Type: application/json" \
      -d '{
        "email": "you@example.com",
        "name": "Your Name",
        "tier": "solo"
      }'
    ```

    ```python Python
    import httpx

    resp = httpx.post(
        "https://api.boringmarketing.com/auth/checkout",
        json={
            "email": "you@example.com",
            "name": "Your Name",
            "tier": "solo",
        },
    )
    checkout = resp.json()
    print(checkout)
    ```

    ```javascript Node.js
    const resp = await fetch(
      "https://api.boringmarketing.com/auth/checkout",
      {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          email: "you@example.com",
          name: "Your Name",
          tier: "solo",
        }),
      }
    );
    const checkout = await resp.json();
    console.log(checkout);
    ```
    </CodeGroup>

    **Expected response:**

    ```json
    {
      "status": "email_sent",
      "session_id": "bmc_k7f3...",
      "poll_url": "https://api.boringmarketing.com/auth/checkout/bmc_k7f3.../status",
      "sent_at": "2026-04-08T18:42:11.123Z"
    }
    ```

    **Check your inbox.** You will receive an email from Boring Marketing with a Stripe checkout link. Click the link, complete payment, then come back to your terminal.

    After payment, poll `poll_url` until the status flips to `completed`:

    <CodeGroup>
    ```bash cURL
    # Poll every 5 seconds until payment is confirmed
    SESSION_ID="bmc_k7f3..."  # from the response above
    while true; do
      RESP=$(curl -s "https://api.boringmarketing.com/auth/checkout/$SESSION_ID/status")
      STATUS=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['status'])")
      echo "Status: $STATUS"
      if [ "$STATUS" = "completed" ]; then
        echo "$RESP" | python3 -m json.tool
        break
      fi
      sleep 5
    done
    ```

    ```python Python
    import time

    poll_url = checkout["poll_url"]

    while True:
        resp = httpx.get(poll_url).json()
        print(f"Status: {resp['status']}")
        if resp["status"] == "completed":
            print(resp)
            break
        time.sleep(5)
    ```

    ```javascript Node.js
    while (true) {
      const resp = await fetch(checkout.poll_url).then(r => r.json());
      console.log(`Status: ${resp.status}`);
      if (resp.status === "completed") {
        console.log(resp);
        break;
      }
      await new Promise(r => setTimeout(r, 5000));
    }
    ```
    </CodeGroup>

    **Completed response:**

    ```json
    {
      "status": "completed",
      "dashboard_url": "https://dashboard.boringmarketing.com"
    }
    ```

    <Note>
    **Path split.** The poll endpoint deliberately never returns an API key — the key has to come through a human-in-the-loop sign-in.

    **If you're a human:** open `dashboard_url`, enter your email to get a magic-link, click the link, copy your key from **Settings → API Access**, and export it:

    ```bash
    export BM_API_KEY="bm_your_key_here"
    ```

    **If you're an agent running unattended:** you cannot finish this step alone in anonymous mode. Surface `dashboard_url` to your operator and wait for them to paste the key back. To skip the handoff entirely, use [authenticated mode on `POST /auth/checkout`](https://docs.boringmarketing.com/api-reference/auth/checkout#authenticated-mode-fast-path) when you already hold a key.
    </Note>
  </Step>

  <Step title="Register your brand">
    Tell the API about your domain. Only two fields required.

    <CodeGroup>
    ```bash cURL
    curl -s -X POST \
      https://api.boringmarketing.com/brands \
      -H "Content-Type: application/json" \
      -H "X-API-Key: $BM_API_KEY" \
      -d '{
        "domain": "yourdomain.com",
        "name": "Your Brand"
      }'
    ```

    ```python Python
    resp = httpx.post(
        "https://api.boringmarketing.com/brands",
        headers={"X-API-Key": api_key},
        json={
            "domain": "yourdomain.com",
            "name": "Your Brand",
        },
    )
    brand = resp.json()
    print(brand)
    ```

    ```javascript Node.js
    const brand = await fetch(
      "https://api.boringmarketing.com/brands",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "X-API-Key": apiKey,
        },
        body: JSON.stringify({
          domain: "yourdomain.com",
          name: "Your Brand",
        }),
      }
    ).then(r => r.json());
    console.log(brand);
    ```
    </CodeGroup>

    **Expected response (201 Created):**

    ```json
    {
      "id": "ea502f67-338b-47e4-a245-5c4eea8bd8d2",
      "domain": "yourdomain.com",
      "name": "Your Brand"
    }
    ```

    The endpoint always returns `201`. If the domain is already registered on your account, the same `201` response includes `"existing": true` so you can detect and short-circuit the duplicate. Export the brand ID for the next steps:

    ```bash
    export BRAND_ID="ea502f67-338b-47e4-a245-5c4eea8bd8d2"
    ```
  </Step>

  <Step title="Run the full pipeline">
    This single call triggers the entire intelligence pipeline: competitor discovery, brand enrichment, keyword universe, evidence collection, and leverage scoring.

    <CodeGroup>
    ```bash cURL
    curl -s -X POST \
      https://api.boringmarketing.com/brands/$BRAND_ID/analyze \
      -H "X-API-Key: $BM_API_KEY"
    ```

    ```python Python
    resp = httpx.post(
        f"https://api.boringmarketing.com/brands/{brand['id']}/analyze",
        headers={"X-API-Key": api_key},
    )
    run = resp.json()
    print(run)
    ```

    ```javascript Node.js
    const run = await fetch(
      `https://api.boringmarketing.com/brands/${brand.id}/analyze`,
      {
        method: "POST",
        headers: { "X-API-Key": apiKey },
      }
    ).then(r => r.json());
    console.log(run);
    ```
    </CodeGroup>

    **Expected response (202 Accepted):**

    ```json
    {
      "run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "status": "queued"
    }
    ```

    Export the run ID:

    ```bash
    export RUN_ID="a1b2c3d4-e5f6-7890-abcd-ef1234567890"
    ```
  </Step>

  <Step title="Wait for completion">
    The full pipeline takes 15-30 minutes. Poll the run status to watch it progress. Stage progress is exposed under `meta.current_stage`, not at the top level.

    <CodeGroup>
    ```bash cURL
    # Poll every 15 seconds
    while true; do
      RESULT=$(curl -s \
        -H "X-API-Key: $BM_API_KEY" \
        "https://api.boringmarketing.com/brands/$BRAND_ID/runs/$RUN_ID")

      STATUS=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin)['status'])")
      STAGE=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('meta',{}).get('current_stage',''))")
      DETAIL=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('meta',{}).get('stage_detail',''))")

      echo "[$STATUS] $STAGE — $DETAIL"
      [ "$STATUS" = "done" ] || [ "$STATUS" = "failed" ] && break
      sleep 15
    done
    ```

    ```python Python
    import time

    brand_id = brand["id"]
    run_id = run["run_id"]

    while True:
        resp = httpx.get(
            f"https://api.boringmarketing.com/brands/{brand_id}/runs/{run_id}",
            headers={"X-API-Key": api_key},
        ).json()
        meta = resp.get("meta") or {}
        print(f"[{resp['status']}] {meta.get('current_stage', '')} — {meta.get('stage_detail', '')}")
        if resp["status"] in ("done", "failed"):
            break
        time.sleep(15)
    ```

    ```javascript Node.js
    const brandId = brand.id;
    const runId = run.run_id;

    while (true) {
      const resp = await fetch(
        `https://api.boringmarketing.com/brands/${brandId}/runs/${runId}`,
        { headers: { "X-API-Key": apiKey } }
      ).then(r => r.json());

      const stage = resp.meta?.current_stage ?? "";
      const detail = resp.meta?.stage_detail ?? "";
      console.log(`[${resp.status}] ${stage} — ${detail}`);
      if (resp.status === "done" || resp.status === "failed") break;
      await new Promise(r => setTimeout(r, 15_000));
    }
    ```
    </CodeGroup>

    **You will see stages progress through these six values:**

    ```
    [running] brand_enrichment — crawling brand site + extracting identity
    [running] competitor_discovery — finding competitors via Tavily
    [running] family_discovery — building keyword universe
    [running] evidence_collection — analyzing SERPs per family
    [running] leverage_scoring — cross-referencing brand gaps with opportunities
    [running] queue_building — assembling ranked action queue
    [done]
    ```

    <Tip>
    While the pipeline runs, this is a good time to read the [Pipeline Overview](https://docs.boringmarketing.com/concepts/pipeline-overview) to understand what each stage does.
    </Tip>
  </Step>

  <Step title="See your opportunities">
    Once the run completes, fetch your ranked content opportunities.

    <CodeGroup>
    ```bash cURL
    curl -s \
      -H "X-API-Key: $BM_API_KEY" \
      "https://api.boringmarketing.com/brands/$BRAND_ID/opportunities" \
      | python3 -m json.tool
    ```

    ```python Python
    opps = httpx.get(
        f"https://api.boringmarketing.com/brands/{brand_id}/opportunities",
        headers={"X-API-Key": api_key},
    ).json()

    for opp in opps["opportunities"][:5]:
        print(f"[{opp['priority']:.2f}] {opp['play']} — {opp['summary']}")
    ```

    ```javascript Node.js
    const opps = await fetch(
      `https://api.boringmarketing.com/brands/${brandId}/opportunities`,
      { headers: { "X-API-Key": apiKey } }
    ).then(r => r.json());

    opps.opportunities.slice(0, 5).forEach(opp => {
      console.log(`[${opp.priority.toFixed(2)}] ${opp.play} — ${opp.summary}`);
    });
    ```
    </CodeGroup>

    **Example response:**

    ```json
    {
      "brand_id": "ea502f67-338b-47e4-a245-5c4eea8bd8d2",
      "total": 42,
      "opportunities": [
        {
          "id": "opp-uuid-1",
          "family": "Marketing Automation",
          "play": "AI Marketing Tools Comparison Guide",
          "summary": "No ranking content for high-intent 'ai marketing tools' cluster",
          "keywords": [
            { "keyword": "ai marketing tools", "google_vol": 18000 },
            { "keyword": "best ai marketing software", "google_vol": 14500 },
            { "keyword": "ai content marketing", "google_vol": 12500 }
          ],
          "priority": 0.92,
          "immediacy": 0.85,
          "ceiling": 0.78,
          "total_volume": 45000,
          "recommended_format": "comparison"
        }
      ]
    }
    ```

    <Check>
    You now have a ranked list of content opportunities, each with a priority score, target keywords, and a recommended format. The API told you exactly what to write and why.
    </Check>
  </Step>
</Steps>

## What just happened

In five steps, the API:

1. **Discovered competitors** in your space automatically
2. **Built a keyword universe** grouped into topic families
3. **Collected evidence** from SERPs, AI Overviews, and LLM citations for each family
4. **Synthesized opportunities** — scored by immediacy (how easy to win) and ceiling (how big the prize)
5. **Built an action queue** — ranked by priority, ready for brief generation

## What's next

<Columns cols={2}>
  <Card title="Generate a Brief" icon="file-lines" href="/guides/content-production">
    Turn your top opportunity into a detailed content brief.
  </Card>
  <Card title="Pipeline Overview" icon="diagram-project" href="/concepts/pipeline-overview">
    Understand each stage and when to run them individually.
  </Card>
  <Card title="Agent Integration" icon="robot" href="/guides/agent-integration">
    Let Claude Code or Cursor run the pipeline for you.
  </Card>
  <Card title="Track Outcomes" icon="chart-line" href="/guides/tracking-outcomes">
    Monitor rankings and LLM citations after publishing.
  </Card>
</Columns>

## Troubleshooting

<AccordionGroup>
  <Accordion title="401 — authentication_required">
    Your `X-API-Key` header is missing or malformed. Make sure you exported it:

    ```bash
    export BM_API_KEY="bm_your_key_here"
    ```

    The key must start with `bm_`.
  </Accordion>
  <Accordion title="402 — credits_exhausted">
    You have hit the daily call limit for your tier. Upgrade via the billing portal:

    ```bash
    curl -s -X POST \
      https://api.boringmarketing.com/auth/billing-portal \
      -H "X-API-Key: $BM_API_KEY"
    ```

    Or sign into the [dashboard](https://dashboard.boringmarketing.com) to change plans.
  </Accordion>
  <Accordion title="409 — Pipeline already running">
    A pipeline run is already in progress for this brand. Check the `X-Running-Run-Id` response header for the existing run ID, then poll that instead.

    ```bash
    curl -s \
      -H "X-API-Key: $BM_API_KEY" \
      "https://api.boringmarketing.com/brands/$BRAND_ID/runs"
    ```
  </Accordion>
  <Accordion title="Pipeline seems stuck">
    The full pipeline takes 15-30 minutes depending on your keyword universe size. Check `meta.current_stage` and `meta.stage_started_at` in the run status — if the stage has been running for more than ~15 minutes, the run may be stuck. Cancel it with `POST /brands/{brand_id}/runs/{run_id}/cancel` and re-trigger.
  </Accordion>
</AccordionGroup>
