> ## Documentation Index
> Fetch the complete documentation index at: https://developers.semji.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Get SEO & GEO recommendations from a keyword

> Create a draft from a focus keyword, run a keyword analysis, and pull back the typed SEO (Google Search) and GEO (Google AI Overview) recommendations.

This guide shows you how to go from a single keyword to a complete brief of SEO and GEO recommendations. The flow uses four endpoints:

1. **Create a draft content** in your editorial planning.
2. **Add the focus keyword** to the draft's page and set it as the focus keyword.
3. **Launch the keyword analysis** asynchronously.
4. **Generate the analysis report** to retrieve typed recommendations.

The same flow powers the *New content* button in the Semji app.

## Prerequisites

* An API key. See [Authentication](/api-reference/authentication).
* The **workspace ID** you want to plan content in. Get it from [`GET /v1/workspaces`](/api-reference/workspaces/list-workspaces).
* A focus keyword you want recommendations for (e.g. `best crm for small business`).

## 1. Create a draft content

Call [`POST /v1/workspaces/{workspaceId}/contents`](/api-reference/contents/create-a-content) without a `pageId` — Semji will auto-create a blank page to host the draft. You only need a `title` to get started.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const API = "https://api.semji.com/v1";
  const HEADERS = {
    Authorization: `Bearer ${process.env.SEMJI_API_KEY}`,
    "Content-Type": "application/json",
  };
  const WORKSPACE_ID = process.env.WORKSPACE_ID;

  const content = await (
    await fetch(`${API}/workspaces/${WORKSPACE_ID}/contents`, {
      method: "POST",
      headers: HEADERS,
      body: JSON.stringify({ title: "Best CRM for small business" }),
    })
  ).json();

  const contentId = content.id;
  const pageId = content.page.id;
  ```

  ```python Python theme={null}
  import os, requests

  API = "https://api.semji.com/v1"
  HEADERS = {"Authorization": f"Bearer {os.environ['SEMJI_API_KEY']}"}
  WORKSPACE_ID = os.environ["WORKSPACE_ID"]

  content = requests.post(
      f"{API}/workspaces/{WORKSPACE_ID}/contents",
      headers=HEADERS,
      json={"title": "Best CRM for small business"},
  ).json()

  content_id = content["id"]
  page_id = content["page"]["id"]
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.semji.com/v1/workspaces/$WORKSPACE_ID/contents \
    -H "Authorization: Bearer $SEMJI_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"title": "Best CRM for small business"}'
  ```
</CodeGroup>

A successful response includes both the new content ID and the auto-created page ID:

```json title="201 Created (excerpt)" theme={null}
{
  "id": "7c4a1f08b29d",
  "title": "Best CRM for small business",
  "page": { "id": "5e8d203c7f1a", "url": null },
  "contentStatus": { "id": "b3d51e92c804", "label": "to do" },
  "version": 1
}
```

Keep both `content.id` and `page.id` — you'll use them in the next steps.

## 2. Attach the focus keyword

Adding a focus keyword is a two-step operation:

1. Add the keyword to the page with [`POST /v1/pages/{pageId}/keywords`](/api-reference/keywords/add-a-keyword-to-a-page).
2. Mark it as the page's focus keyword with [`PUT /v1/pages/{id}`](/api-reference/pages/update-a-page).

<CodeGroup>
  ```typescript TypeScript theme={null}
  // 1. Create the keyword on the page
  const keyword = await (
    await fetch(`${API}/pages/${pageId}/keywords`, {
      method: "POST",
      headers: HEADERS,
      body: JSON.stringify({ keyword: "best crm for small business" }),
    })
  ).json();
  const keywordId = keyword.id;

  // 2. Set it as the page's focus keyword
  const update = await fetch(`${API}/pages/${pageId}`, {
    method: "PUT",
    headers: HEADERS,
    body: JSON.stringify({ focusKeywordId: keywordId }),
  });
  if (!update.ok) throw new Error(`PUT failed: ${update.status}`);
  ```

  ```python Python theme={null}
  keyword = requests.post(
      f"{API}/pages/{page_id}/keywords",
      headers=HEADERS,
      json={"keyword": "best crm for small business"},
  ).json()
  keyword_id = keyword["id"]

  requests.put(
      f"{API}/pages/{page_id}",
      headers=HEADERS,
      json={"focusKeywordId": keyword_id},
  ).raise_for_status()
  ```

  ```bash cURL theme={null}
  # 1. Create the keyword on the page
  KEYWORD=$(curl -X POST https://api.semji.com/v1/pages/$PAGE_ID/keywords \
    -H "Authorization: Bearer $SEMJI_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"keyword": "best crm for small business"}')
  KEYWORD_ID=$(echo $KEYWORD | jq -r .id)

  # 2. Set it as the page's focus keyword
  curl -X PUT https://api.semji.com/v1/pages/$PAGE_ID \
    -H "Authorization: Bearer $SEMJI_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"focusKeywordId\": \"$KEYWORD_ID\"}"
  ```
</CodeGroup>

<Tip>
  If the keyword already exists in the workspace, `POST /v1/pages/{pageId}/keywords` reuses it instead of creating a duplicate.
</Tip>

## 3. Launch the keyword analysis

Recommendations are not computed on demand — you have to launch an asynchronous analysis with [`POST /v1/keywords/{id}/analyze`](/api-reference/keywords/launch-keyword-analysis). The analysis scrapes the Google SERP, runs the GEO/AI Overview probe, and stores the typed recommendations on the keyword.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const analyze = await fetch(`${API}/keywords/${keywordId}/analyze`, {
    method: "POST",
    headers: HEADERS,
  });
  if (!analyze.ok) throw new Error(`analyze failed: ${analyze.status}`);
  ```

  ```python Python theme={null}
  requests.post(
      f"{API}/keywords/{keyword_id}/analyze", headers=HEADERS,
  ).raise_for_status()
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.semji.com/v1/keywords/$KEYWORD_ID/analyze \
    -H "Authorization: Bearer $SEMJI_API_KEY"
  ```
</CodeGroup>

The endpoint returns `202 Accepted` immediately and the analysis runs in the background. Poll [`GET /v1/keywords/{id}`](/api-reference/keywords/get-keyword-details) until `analysisStatus` is `"success"`:

<CodeGroup>
  ```typescript Polling loop (TypeScript) theme={null}
  while (true) {
    const keyword = await (
      await fetch(`${API}/keywords/${keywordId}`, { headers: HEADERS })
    ).json();
    if (keyword.analysisStatus === "success") break;
    if (keyword.analysisStatus === "failed") throw new Error("Keyword analysis failed");
    await new Promise((resolve) => setTimeout(resolve, 5_000));
  }
  ```

  ```python Polling loop (Python) theme={null}
  import time

  while True:
      keyword = requests.get(f"{API}/keywords/{keyword_id}", headers=HEADERS).json()
      if keyword["analysisStatus"] == "success":
          break
      if keyword["analysisStatus"] == "failed":
          raise RuntimeError("Keyword analysis failed")
      time.sleep(5)
  ```
</CodeGroup>

`analysisStatus` transitions through `queued` → `pending` → `success` (or `failed`). Most analyses complete within 30 seconds.

<Note>
  Each analysis consumes one **analysis credit** from your organization's balance. Check available credits with [`GET /v1/me`](/api-reference/me/get-authenticated-user) (look at `organization.credits.analysis`).
</Note>

## 4. Retrieve the SEO & GEO recommendations

Once the analysis is `success`, call [`POST /v1/keywords/{id}/report`](/api-reference/keywords/generate-keyword-analysis-report) to score a content draft against the analysis and pull the typed recommendations.

You have two ways to score:

* **By reference** — pass `contentId` and Semji uses the draft's current `title` + `html`.
* **By value** — pass `title` and `html` inline (useful for previewing recommendations against arbitrary text).

<CodeGroup>
  ```typescript TypeScript theme={null}
  const report = await (
    await fetch(`${API}/keywords/${keywordId}/report`, {
      method: "POST",
      headers: HEADERS,
      body: JSON.stringify({ contentId }),
    })
  ).json();
  ```

  ```python Python theme={null}
  report = requests.post(
      f"{API}/keywords/{keyword_id}/report",
      headers=HEADERS,
      json={"contentId": content_id},
  ).json()
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.semji.com/v1/keywords/$KEYWORD_ID/report \
    -H "Authorization: Bearer $SEMJI_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"contentId\": \"$CONTENT_ID\"}"
  ```
</CodeGroup>

The response contains two top-level surfaces:

* `googleSearch` — classic SEO recommendations from the SERP (topics, questions, search intents, internal links to add, SERP competitors).
* `googleAiOverview` — GEO recommendations from the AI Overview answer (topics to cover for citation, cited sources, mentioned brands, markdown preview of the LLM answer).

```json title="200 OK (excerpt)" theme={null}
{
  "googleSearch": {
    "score": 0.42,
    "recommendations": {
      "topicsSuggestion": { "score": 0.55, "items": [{ "topic": "pricing", "...": "..." }] },
      "questionsSuggestion": { "score": 0.30, "items": [{ "question": "Which CRM is free?" }] }
    },
    "competitors": [{ "domain": "hubspot.com", "position": 1 }]
  },
  "googleAiOverview": {
    "score": 0.31,
    "recommendations": {
      "geoTopicsSuggestion": { "score": 0.31, "items": [{ "topic": "integration with email" }] }
    },
    "sources": [{ "domain": "salesforce.com", "position": 1 }],
    "brands": [{ "name": "HubSpot", "count": 4, "category": "software" }],
    "preview": "## Best CRM for small business…"
  }
}
```

Each `*Suggestion` block carries its own `score` (0.0 – 1.0) and a list of `items` you can render in your brief. The top-level `score` on each surface is the overall match between the content and the recommendations.

<Tip>
  Re-call the report endpoint as the draft evolves — the recommendations are fixed (until you re-run the analysis), but the scores change as the `html` improves.
</Tip>

## Putting it all together

<CodeGroup>
  ```typescript recommendations-from-keyword.ts expandable theme={null}
  const API = "https://api.semji.com/v1";
  const HEADERS = {
    Authorization: `Bearer ${process.env.SEMJI_API_KEY}`,
    "Content-Type": "application/json",
  };
  const WORKSPACE_ID = process.env.WORKSPACE_ID;

  async function api(path: string, init: RequestInit = {}) {
    const response = await fetch(`${API}${path}`, { ...init, headers: HEADERS });
    if (!response.ok) {
      throw new Error(`${init.method ?? "GET"} ${path} → ${response.status}`);
    }
    return response.json();
  }

  async function createDraft(title: string) {
    return api(`/workspaces/${WORKSPACE_ID}/contents`, {
      method: "POST",
      body: JSON.stringify({ title }),
    });
  }

  async function setFocusKeyword(pageId: string, keyword: string) {
    const created = await api(`/pages/${pageId}/keywords`, {
      method: "POST",
      body: JSON.stringify({ keyword }),
    });
    await api(`/pages/${pageId}`, {
      method: "PUT",
      body: JSON.stringify({ focusKeywordId: created.id }),
    });
    return created.id;
  }

  async function analyzeAndWait(keywordId: string) {
    await api(`/keywords/${keywordId}/analyze`, { method: "POST" });
    while (true) {
      const keyword = await api(`/keywords/${keywordId}`);
      if (keyword.analysisStatus === "success") return;
      if (keyword.analysisStatus === "failed") throw new Error("Keyword analysis failed");
      await new Promise((resolve) => setTimeout(resolve, 5_000));
    }
  }

  async function getReport(keywordId: string, contentId: string) {
    return api(`/keywords/${keywordId}/report`, {
      method: "POST",
      body: JSON.stringify({ contentId }),
    });
  }

  async function recommendationsFor(keyword: string) {
    const draft = await createDraft(keyword.charAt(0).toUpperCase() + keyword.slice(1));
    const keywordId = await setFocusKeyword(draft.page.id, keyword);
    await analyzeAndWait(keywordId);
    return getReport(keywordId, draft.id);
  }

  const report = await recommendationsFor("best crm for small business");
  console.log("SEO score:", report.googleSearch.score);
  console.log("GEO score:", report.googleAiOverview.score);
  ```

  ```python recommendations_from_keyword.py expandable theme={null}
  import os
  import time
  import requests

  API = "https://api.semji.com/v1"
  HEADERS = {"Authorization": f"Bearer {os.environ['SEMJI_API_KEY']}"}
  WORKSPACE_ID = os.environ["WORKSPACE_ID"]

  def create_draft(title):
      return requests.post(
          f"{API}/workspaces/{WORKSPACE_ID}/contents",
          headers=HEADERS, json={"title": title},
      ).json()

  def set_focus_keyword(page_id, keyword):
      kw = requests.post(
          f"{API}/pages/{page_id}/keywords",
          headers=HEADERS, json={"keyword": keyword},
      ).json()
      requests.put(
          f"{API}/pages/{page_id}",
          headers=HEADERS, json={"focusKeywordId": kw["id"]},
      ).raise_for_status()
      return kw["id"]

  def analyze_and_wait(keyword_id):
      requests.post(f"{API}/keywords/{keyword_id}/analyze", headers=HEADERS).raise_for_status()
      while True:
          kw = requests.get(f"{API}/keywords/{keyword_id}", headers=HEADERS).json()
          if kw["analysisStatus"] == "success":
              return
          if kw["analysisStatus"] == "failed":
              raise RuntimeError("Keyword analysis failed")
          time.sleep(5)

  def get_report(keyword_id, content_id):
      return requests.post(
          f"{API}/keywords/{keyword_id}/report",
          headers=HEADERS, json={"contentId": content_id},
      ).json()

  def recommendations_for(keyword):
      draft = create_draft(title=keyword.capitalize())
      keyword_id = set_focus_keyword(draft["page"]["id"], keyword)
      analyze_and_wait(keyword_id)
      return get_report(keyword_id, draft["id"])

  if __name__ == "__main__":
      report = recommendations_for("best crm for small business")
      print("SEO score:", report["googleSearch"]["score"])
      print("GEO score:", report["googleAiOverview"]["score"])
  ```
</CodeGroup>

## Reference

* [Create a content](/api-reference/contents/create-a-content)
* [Add a keyword to a page](/api-reference/keywords/add-a-keyword-to-a-page)
* [Update a page](/api-reference/pages/update-a-page)
* [Launch keyword analysis](/api-reference/keywords/launch-keyword-analysis)
* [Get keyword details](/api-reference/keywords/get-keyword-details)
* [Generate keyword analysis report](/api-reference/keywords/generate-keyword-analysis-report)
