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

# Knowledge Base → Brand Voice

### Scenario

Your company's Notion knowledge base — product docs, style guides, internal memos, blog drafts — represents your authentic voice. Instead of manually defining brand guidelines, you extract them from how your team actually writes. This job pulls pages from a knowledge base database, aggregates the text, and creates a Mavera Brand Voice that captures your organization's natural tone, vocabulary, and patterns.

**Flow:** Notion `POST /databases/{id}/query` (knowledge base) → `GET /blocks/{page_id}/children` per page → Aggregate text → Mavera `POST /api/v1/brand-voices` → Brand Voice profile

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, requests, time

  NOTION = os.environ["NOTION_API_KEY"]
  MV = os.environ["MAVERA_API_KEY"]
  NB = "https://api.notion.com/v1"
  MB = "https://app.mavera.io/api/v1"
  NH = {
      "Authorization": f"Bearer {NOTION}",
      "Notion-Version": "2022-06-28",
      "Content-Type": "application/json",
  }
  MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  KB_DB_ID = "your-knowledge-base-database-id"
  CATEGORIES = ["Product Docs", "Blog Drafts", "Style Guide", "Internal Comms"]

  # 1. Query knowledge base pages
  all_pages = []
  for category in CATEGORIES:
      r = requests.post(f"{NB}/databases/{KB_DB_ID}/query", headers=NH, json={
          "filter": {"property": "Category", "select": {"equals": category}},
          "sorts": [{"property": "Last edited time", "direction": "descending"}],
          "page_size": 10,
      })
      if r.status_code == 429:
          time.sleep(1)
          r = requests.post(f"{NB}/databases/{KB_DB_ID}/query", headers=NH, json={
              "filter": {"property": "Category", "select": {"equals": category}},
              "page_size": 10,
          })
      data = r.json()
      all_pages.extend(data.get("results", []))
      time.sleep(0.4)

  print(f"Found {len(all_pages)} knowledge base pages across {len(CATEGORIES)} categories")

  # 2. Extract text from pages
  def get_blocks_text(block_id):
      texts = []
      cursor = None
      while True:
          params = {"page_size": 100}
          if cursor:
              params["start_cursor"] = cursor
          r = requests.get(f"{NB}/blocks/{block_id}/children", headers=NH, params=params)
          if r.status_code == 429:
              time.sleep(1); continue
          r.raise_for_status()
          data = r.json()
          for block in data.get("results", []):
              btype = block.get("type", "")
              rt = block.get(btype, {}).get("rich_text", [])
              text = "".join(t.get("plain_text", "") for t in rt)
              if text.strip():
                  texts.append(text)
          cursor = data.get("next_cursor")
          if not cursor:
              break
          time.sleep(0.4)
      return "\n".join(texts)

  samples = []
  for page in all_pages[:25]:
      props = page.get("properties", {})
      title_parts = props.get("Name", props.get("Title", {})).get("title", [])
      title = "".join(t.get("plain_text", "") for t in title_parts)
      text = get_blocks_text(page["id"])
      if len(text) > 100:
          samples.append(f"=== {title} ===\n{text[:1500]}")

  combined = "\n\n---\n\n".join(samples)
  print(f"Collected {len(samples)} pages ({len(combined)} chars)")

  # 3. Create Brand Voice
  bv = requests.post(f"{MB}/brand-voices", headers=MH, json={
      "name": "Notion Knowledge Base Voice",
      "samples": [combined[:50000]],
      "description": (
          f"Extracted from {len(samples)} internal knowledge base pages "
          f"across categories: {', '.join(CATEGORIES)}. "
          "Captures organizational writing patterns, technical vocabulary, and tone."
      ),
  }).json()

  print(f"Brand Voice: {bv.get('id', 'error')}")

  # 4. Wait and inspect
  time.sleep(5)
  detail = requests.get(f"{MB}/brand-voices/{bv['id']}", headers=MH).json()
  print(f"Status: {detail.get('status', 'unknown')}")
  if detail.get("traits"):
      print(f"Traits: {detail['traits']}")

  # 5. Test with a generation
  from openai import OpenAI
  mavera = OpenAI(api_key=MV, base_url=MB)
  test = mavera.responses.create(
      model="mavera-1",
      input=[{"role": "user", "content": "Write a 100-word product update announcement for a new API versioning feature."}],
      extra_body={"brand_voice_id": bv["id"]},
  )
  print(f"\nTest generation:\n{test.output[0].content[0].text}")
  ```

  ```javascript JavaScript theme={"dark"}
  const OpenAI = require("openai").default;
  const NOTION = process.env.NOTION_API_KEY;
  const MV = process.env.MAVERA_API_KEY;
  const NB = "https://api.notion.com/v1";
  const MB = "https://app.mavera.io/api/v1";
  const NH = {
    Authorization: `Bearer ${NOTION}`,
    "Notion-Version": "2022-06-28",
    "Content-Type": "application/json",
  };
  const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  const KB_DB_ID = "your-knowledge-base-database-id";
  const CATEGORIES = ["Product Docs", "Blog Drafts", "Style Guide", "Internal Comms"];

  // 1. Query pages per category
  const allPages = [];
  for (const category of CATEGORIES) {
    const res = await fetch(`${NB}/databases/${KB_DB_ID}/query`, {
      method: "POST", headers: NH,
      body: JSON.stringify({
        filter: { property: "Category", select: { equals: category } },
        sorts: [{ property: "Last edited time", direction: "descending" }],
        page_size: 10,
      }),
    });
    if (res.status === 429) await new Promise(r => setTimeout(r, 1000));
    const data = await (res.status === 429
      ? fetch(`${NB}/databases/${KB_DB_ID}/query`, { method: "POST", headers: NH,
          body: JSON.stringify({ filter: { property: "Category", select: { equals: category } }, page_size: 10 }),
        })
      : Promise.resolve(res)).then(r => r.json());
    allPages.push(...(data.results || []));
    await new Promise(r => setTimeout(r, 400));
  }

  console.log(`Found ${allPages.length} pages across ${CATEGORIES.length} categories`);

  // 2. Extract text
  async function getBlocksText(blockId) {
    const texts = [];
    let cursor = null;
    do {
      const params = new URLSearchParams({ page_size: "100" });
      if (cursor) params.set("start_cursor", cursor);
      const res = await fetch(`${NB}/blocks/${blockId}/children?${params}`, { headers: NH });
      if (res.status === 429) { await new Promise(r => setTimeout(r, 1000)); continue; }
      const data = await res.json();
      for (const block of data.results || []) {
        const text = (block[block.type]?.rich_text || []).map(t => t.plain_text).join("");
        if (text.trim()) texts.push(text);
      }
      cursor = data.next_cursor;
      await new Promise(r => setTimeout(r, 400));
    } while (cursor);
    return texts.join("\n");
  }

  const samples = [];
  for (const page of allPages.slice(0, 25)) {
    const props = page.properties || {};
    const titleProp = props.Name || props.Title || {};
    const title = (titleProp.title || []).map(t => t.plain_text).join("");
    const text = await getBlocksText(page.id);
    if (text.length > 100) samples.push(`=== ${title} ===\n${text.slice(0, 1500)}`);
  }

  const combined = samples.join("\n\n---\n\n");
  console.log(`Collected ${samples.length} pages (${combined.length} chars)`);

  // 3. Brand Voice
  const bv = await fetch(`${MB}/brand-voices`, { method: "POST", headers: MH,
    body: JSON.stringify({
      name: "Notion Knowledge Base Voice",
      samples: [combined.slice(0, 50000)],
      description: `From ${samples.length} pages across: ${CATEGORIES.join(", ")}.`,
    }),
  }).then(r => r.json());
  console.log(`Brand Voice: ${bv.id}`);

  // 4. Test
  await new Promise(r => setTimeout(r, 5000));
  const mavera = new OpenAI({ apiKey: MV, baseURL: MB });
  const test = await mavera.responses.create({
    model: "mavera-1",
    input: [{ role: "user", content: "Write a 100-word product update for a new API versioning feature." }],
    extra_body: { brand_voice_id: bv.id },
  });
  console.log(`\nTest:\n${test.output[0].content[0].text}`);
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
Found 32 knowledge base pages across 4 categories
Collected 25 pages (28734 chars)
Brand Voice: bv_notion_kb_4x7m (status: ready)
Traits: Direct, technically precise, confident without jargon. Uses
        short sentences. Favors "you" over "the user". Active voice.
        Contractions in informal content, formal in docs.

Test generation:
We shipped API versioning today. Every endpoint now accepts a version
header — v1 stays stable while v2 rolls out. Your existing integrations
won't break. Migration is opt-in: add `X-API-Version: 2` when you're
ready. The big changes: nested filters, cursor pagination by default,
and typed error responses. We wrote a migration guide (3-minute read)
and updated every SDK. Questions? Hit us in #api-support.
```

### Error Handling

<AccordionGroup>
  <Accordion title="Category property missing">If your KB database doesn't have a `Category` select property, query without the filter and use `page_size: 30` to get a broad sample across all page types.</Accordion>
  <Accordion title="Insufficient sample size">Brand Voice extraction works best with 5,000+ words across varied content types. If you have fewer than 10 pages, supplement with published blog URLs via the `urls` field.</Accordion>
  <Accordion title="Rich text formatting lost">The extraction captures plain text only. Bold, italic, and code formatting are stripped. This is intentional — Brand Voice focuses on language patterns, not formatting.</Accordion>
</AccordionGroup>
