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 company publishes polished updates, but employees don’t reshare them — the posts sound corporate. This job pulls recent company updates, creates or reuses a conversational Brand Voice, then generates employee-ready versions of each post. The rewrites swap corporate tone for personal, first-person language that employees can paste directly into their feeds. Each variant is tailored for individual sharing: shorter, more conversational, with a personal take that makes the reshare feel authentic.

Architecture

Code

import os, requests, time

LI = os.environ["LINKEDIN_ACCESS_TOKEN"]
MV = os.environ["MAVERA_API_KEY"]
LI_BASE = "https://api.linkedin.com/rest"
MV_BASE = "https://app.mavera.io/api/v1"
LI_H = {"Authorization": f"Bearer {LI}", "LinkedIn-Version": "202401", "X-Restli-Protocol-Version": "2.0.0"}
MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

ORG_URN = "urn:li:organization:12345678"

# 1. Pull recent company posts
r = requests.get(f"{LI_BASE}/posts",
    headers=LI_H,
    params={"q": "author", "author": ORG_URN, "count": 20, "sortBy": "LAST_MODIFIED"})
if r.status_code == 429:
    time.sleep(int(r.headers.get("Retry-After", 60)))
    r = requests.get(f"{LI_BASE}/posts", headers=LI_H,
        params={"q": "author", "author": ORG_URN, "count": 20, "sortBy": "LAST_MODIFIED"})
r.raise_for_status()
posts = r.json().get("elements", [])

# 2. Filter to shareable posts (skip internal-only or short updates)
shareable = []
for post in posts:
    commentary = post.get("commentary", "")
    visibility = post.get("visibility", "")
    if not commentary or len(commentary) < 50:
        continue
    shareable.append({
        "urn": post.get("id", ""),
        "commentary": commentary,
        "content_type": post.get("content", {}).get("contentType", "NONE"),
        "media_url": post.get("content", {}).get("media", {}).get("id", ""),
    })

if not shareable:
    raise SystemExit("No shareable posts found.")

# 3. Create conversational Brand Voice for employee shares
employee_voice_samples = """
I've been at [Company] for two years now, and this is the kind of work that
makes me proud to be here. We just shipped something that actually changes
how our customers work — not incrementally, but fundamentally.

---

Hot take: most marketing analytics tools are built for dashboards, not
decisions. We built the opposite. Here's what I mean.

---

Three things I learned this quarter working with our customers:
1. The data isn't the problem — it's getting from data to action
2. Speed matters more than precision in most marketing decisions
3. Teams that automate reporting spend 60% more time on strategy

This is why I love what we're building.
"""

bv = requests.post(f"{MV_BASE}/brand-voices", headers=MV_H, json={
    "name": "Employee Advocacy Voice — Conversational",
    "samples": [employee_voice_samples],
}).json()
voice_id = bv["id"]
print(f"Brand Voice: {voice_id}")

# 4. Generate employee-ready variants for each post
all_variants = []
for i, post in enumerate(shareable[:5]):
    gen = requests.post(f"{MV_BASE}/generations", headers=MV_H, json={
        "brand_voice_id": voice_id,
        "prompt": f"""Rewrite this LinkedIn company post for individual employees to share on their personal profiles.

ORIGINAL COMPANY POST:
{post['commentary'][:600]}

Rules:
- Write in first person ("I", "we", "my team")
- Add a personal take or reaction (not just a reshare)
- Keep under 200 words (LinkedIn mobile truncation)
- Open with a hook — not "Excited to announce" or "Thrilled to share"
- End with a question or invitation to discuss
- Sound like a real person, not a press release
- Generate 3 variants with different angles: (1) personal story, (2) hot take, (3) lesson learned

CONTENT TYPE: {post['content_type']}""",
        "count": 3,
    }).json()

    variants = gen.get("results", [gen])
    all_variants.append({
        "original": post["commentary"][:200],
        "urn": post["urn"],
        "variants": [v.get("content", v.get("text", ""))[:500] for v in variants],
    })
    time.sleep(0.5)

# 5. Output
for entry in all_variants:
    print(f"\n{'='*60}")
    print(f"ORIGINAL: {entry['original'][:120]}...")
    print(f"{'='*60}")
    for j, variant in enumerate(entry["variants"]):
        labels = ["Personal Story", "Hot Take", "Lesson Learned"]
        print(f"\n  [{labels[j] if j < len(labels) else f'Variant {j+1}'}]")
        print(f"  {variant}")

print(f"\n--- Generated {sum(len(e['variants']) for e in all_variants)} employee variants for {len(all_variants)} posts ---")
print(f"Brand Voice: {voice_id}")

Example Output

Brand Voice: bv_emp_adv_7x3k

============================================================
ORIGINAL: We're proud to announce our Series B funding of $45M led by...
============================================================

  [Personal Story]
  Two years ago I joined a 12-person team with a big bet on marketing
  intelligence. Today we announced our Series B — $45M to keep building.

  What convinced me to stay wasn't the funding. It was watching a customer
  cancel three other tools after one month with us. That's the kind of
  product-market fit you can feel.

  If you're building in MarTech, I'd love to hear — what made you stay at
  your company past year one?

  [Hot Take]
  $45M is great. But here's what matters more than the number:

  We're not raising to find product-market fit. We're raising because
  customers keep asking us to do more. 200+ teams. 94% retention. That's
  the story behind the press release.

  Funding announcements are noise. Retention is signal. What metrics do
  you actually care about when evaluating a vendor?

  [Lesson Learned]
  Three things I've learned on the road to Series B:
  1. Customers who churned taught us more than customers who stayed
  2. The feature nobody asked for became our top differentiator
  3. "Move fast and break things" is wrong — move fast and fix things

  We just raised $45M to keep doing exactly this. What's the most
  counterintuitive lesson from your company's growth?

Error Handling

The employee voice samples define the output tone. Generic samples produce generic rewrites. Include 3–5 real examples of how your best employees already post on LinkedIn.
LinkedIn truncates posts at ~210 characters on mobile with a “see more” fold. The prompt targets under 200 words total, but the hook (first line) should be under 150 characters to display fully.
To avoid creating duplicate voices, check first: GET /api/v1/brand-voices?search=Employee+Advocacy. If one exists, pass its id directly to the generation call.

LinkedIn Content Integration

Brand Voice