Skip to main content

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.

Scenario

Your marketing team maintains a Notion content calendar database with columns like Title, Status, Content Type, Target Persona, and Publish Date. Planned pieces sit in “Idea” or “Outlined” status with no draft. This job queries the database for upcoming content, then calls Mavera’s Generate endpoint for each piece — producing first drafts at scale that match your existing brand voice. Flow: Notion POST /databases/{id}/query (filter: status = “Planned”) → For each row → Mavera POST /api/v1/generations (with brand_voice_id) → Draft content attached back as Notion page

Code

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"}

CALENDAR_DB_ID = "your-content-calendar-database-id"
BRAND_VOICE_ID = "bv_your_voice_id"

# 1. Query planned content from the calendar
results = requests.post(f"{NB}/databases/{CALENDAR_DB_ID}/query", headers=NH, json={
    "filter": {
        "property": "Status",
        "select": {"equals": "Planned"},
    },
    "sorts": [{"property": "Publish Date", "direction": "ascending"}],
    "page_size": 20,
}).json().get("results", [])

print(f"Found {len(results)} planned content pieces")

# 2. Generate a draft for each piece
drafts = []
for page in results:
    props = page.get("properties", {})
    title_parts = props.get("Title", {}).get("title", [])
    title = "".join(t.get("plain_text", "") for t in title_parts) or "Untitled"

    content_type = (props.get("Content Type", {}).get("select", {}) or {}).get("name", "blog post")
    persona = (props.get("Target Persona", {}).get("select", {}) or {}).get("name", "general audience")
    notes_parts = props.get("Notes", {}).get("rich_text", [])
    notes = "".join(t.get("plain_text", "") for t in notes_parts)

    gen = requests.post(f"{MB}/generations", headers=MH, json={
        "brand_voice_id": BRAND_VOICE_ID,
        "prompt": (
            f"Write a {content_type} titled '{title}' for {persona}. "
            f"{'Brief: ' + notes if notes else 'No additional brief.'} "
            f"Include an engaging intro, 3-5 sections with headers, and a conclusion with CTA. "
            f"Length: 800-1200 words."
        ),
    }).json()

    draft_text = gen.get("output", gen.get("content", ""))
    drafts.append({"title": title, "page_id": page["id"], "draft": draft_text})

    # 3. Append draft as a child block in the Notion page
    blocks = []
    for paragraph in draft_text.split("\n\n"):
        paragraph = paragraph.strip()
        if not paragraph:
            continue
        if paragraph.startswith("# "):
            blocks.append({"object": "block", "type": "heading_1",
                "heading_1": {"rich_text": [{"type": "text", "text": {"content": paragraph[2:]}}]}})
        elif paragraph.startswith("## "):
            blocks.append({"object": "block", "type": "heading_2",
                "heading_2": {"rich_text": [{"type": "text", "text": {"content": paragraph[3:]}}]}})
        else:
            for chunk in [paragraph[i:i+2000] for i in range(0, len(paragraph), 2000)]:
                blocks.append({"object": "block", "type": "paragraph",
                    "paragraph": {"rich_text": [{"type": "text", "text": {"content": chunk}}]}})

    if blocks:
        requests.patch(f"{NB}/blocks/{page['id']}/children", headers=NH,
            json={"children": blocks[:100]})

    # 4. Update status to "Draft"
    requests.patch(f"{NB}/pages/{page['id']}", headers=NH, json={
        "properties": {"Status": {"select": {"name": "Draft"}}}
    })

    print(f"  ✓ {title}{len(draft_text)} chars")
    time.sleep(0.5)

print(f"\nGenerated {len(drafts)} drafts")

Example Output

Found 8 planned content pieces
  ✓ Q3 Product Launch Blog → 1847 chars
  ✓ Customer Success Webinar Script → 2103 chars
  ✓ SEO Guide: AI in Marketing → 1654 chars
  ✓ Case Study: Acme Corp → 1422 chars
  ✓ LinkedIn Carousel: 5 Tips → 892 chars
  ✓ Email Nurture: Onboarding Series #3 → 1238 chars
  ✓ Whitepaper: ROI Framework → 3201 chars
  ✓ Social Thread: Product Update → 756 chars

Generated 8 drafts

Error Handling

If the query returns 404 or object not found, open the database in Notion → ⋯ → Connections → add your integration. This is the most common issue.
Notion property names are case-sensitive. If Status is actually status or Content Status in your database, the filter returns zero results. Use GET /databases/{id} to inspect property names.
The loop includes a 500ms delay. For calendars with 50+ pieces, add exponential backoff on 429 or batch into runs of 10.
Notion limits rich_text content to 2000 characters per block. The code chunks long paragraphs. Append requests are limited to 100 blocks per call.