> ## 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.

# Review Response Generation

> Generate personalized, brand-voiced responses to negative Trustpilot reviews using Mavera

## Scenario

Responding to negative reviews is high-stakes — a great response can convert a detractor into an advocate. You pull 1-3 star reviews, load your customer service brand voice from Mavera, then generate personalized responses that acknowledge the specific issue, offer a solution, and maintain your brand's tone. Each response is unique to the review, not a generic template.

**Flow:** Trustpilot `GET /reviews` (1-3 star) → Mavera `POST /brand-voices` or load existing → `POST /generations` (per review) → Personalized response drafts

## Code

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

  TP_KEY = os.environ["TRUSTPILOT_API_KEY"]
  MV = os.environ["MAVERA_API_KEY"]
  BU_ID = os.environ["TRUSTPILOT_BU_ID"]
  TP_BASE = "https://api.trustpilot.com/v1"
  MV_BASE = "https://app.mavera.io/api/v1"
  MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  # 1. Pull negative reviews without replies
  negative = []
  for stars in [1, 2, 3]:
      r = requests.get(f"{TP_BASE}/business-units/{BU_ID}/reviews",
          params={"apikey": TP_KEY, "stars": stars, "perPage": 20,
                   "orderBy": "createdat.desc"})
      r.raise_for_status()
      for rev in r.json().get("reviews", []):
          if not rev.get("companyReply"):
              negative.append(rev)
      time.sleep(0.2)

  print(f"Found {len(negative)} unreplied negative reviews")

  # 2. Create or load customer service brand voice
  CS_VOICE_SAMPLES = [
      "We're sorry to hear about your experience. This isn't the standard we hold ourselves to.",
      "Thank you for taking the time to share this feedback. We take every review seriously.",
      "We'd love to make this right. Please reach out to support@company.com with your order number.",
      "Your satisfaction matters to us, and we're actively working on improving this.",
  ]

  bv = requests.post(f"{MV_BASE}/brand-voices", headers=MV_H, json={
      "name": "Customer Service Voice",
      "samples": ["\n\n---\n\n".join(CS_VOICE_SAMPLES)],
      "preferred_terms": ["we appreciate", "make this right", "your experience matters"],
      "avoid_terms": ["unfortunately", "per our policy", "as stated in our terms"],
  }).json()
  bv_id = bv["id"]
  print(f"Brand Voice: {bv_id}")
  time.sleep(2)

  # 3. Generate responses per review
  responses = []
  for rev in negative[:10]:
      stars = rev.get("stars", 0)
      title = rev.get("title", "No title")
      text = rev.get("text", "")[:500]
      reviewer = rev.get("consumer", {}).get("displayName", "Customer")
      review_id = rev.get("id", "")

      gen = requests.post(f"{MV_BASE}/generations", headers=MV_H, json={
          "brand_voice_id": bv_id,
          "prompt": (
              f"Write a response to this {stars}-star Trustpilot review.\n\n"
              f"Reviewer: {reviewer}\n"
              f"Title: {title}\n"
              f"Review: {text}\n\n"
              f"Rules:\n"
              f"- Acknowledge the SPECIFIC issue (not generic)\n"
              f"- Offer a concrete next step\n"
              f"- Keep under 80 words\n"
              f"- Never be defensive\n"
              f"- Use the customer's name\n"
              f"- End with a way to reach your team"
          ),
      }).json()

      response_text = gen.get("output", gen.get("content", gen.get("text", "")))
      responses.append({
          "review_id": review_id,
          "stars": stars,
          "title": title,
          "response": response_text,
      })
      print(f"\n[{stars}★] {title}")
      print(f"  Review: {text[:100]}...")
      print(f"  Response: {response_text[:200]}")
      time.sleep(0.5)

  # 4. Post responses (requires OAuth — uncomment in production)
  # TP_OAUTH_TOKEN = os.environ["TRUSTPILOT_OAUTH_TOKEN"]
  # for resp in responses:
  #     requests.post(
  #         f"{TP_BASE}/private/reviews/{resp['review_id']}/reply",
  #         headers={"Authorization": f"Bearer {TP_OAUTH_TOKEN}", "Content-Type": "application/json"},
  #         json={"message": resp["response"]})
  ```

  ```javascript JavaScript theme={"dark"}
  const TP_KEY = process.env.TRUSTPILOT_API_KEY;
  const MV = process.env.MAVERA_API_KEY;
  const BU_ID = process.env.TRUSTPILOT_BU_ID;
  const TP_BASE = "https://api.trustpilot.com/v1";
  const MV_BASE = "https://app.mavera.io/api/v1";
  const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  // 1. Unreplied negative reviews
  const negative = [];
  for (const stars of [1, 2, 3]) {
    const res = await fetch(
      `${TP_BASE}/business-units/${BU_ID}/reviews?apikey=${TP_KEY}&stars=${stars}&perPage=20&orderBy=createdat.desc`
    );
    for (const rev of (await res.json()).reviews || []) {
      if (!rev.companyReply) negative.push(rev);
    }
    await new Promise((r) => setTimeout(r, 200));
  }
  console.log(`${negative.length} unreplied negative reviews`);

  // 2. Brand Voice
  const bv = await fetch(`${MV_BASE}/brand-voices`, {
    method: "POST", headers: MV_H,
    body: JSON.stringify({
      name: "CS Voice",
      samples: ["We're sorry about your experience. We take every review seriously.\n\n---\n\nWe'd love to make this right. Reach out to support@company.com."],
      preferred_terms: ["make this right", "your experience matters"],
      avoid_terms: ["unfortunately", "per our policy"],
    }),
  }).then((r) => r.json());
  await new Promise((r) => setTimeout(r, 2000));

  // 3. Generate responses
  for (const rev of negative.slice(0, 10)) {
    const gen = await fetch(`${MV_BASE}/generations`, {
      method: "POST", headers: MV_H,
      body: JSON.stringify({
        brand_voice_id: bv.id,
        prompt: `Respond to this ${rev.stars}-star review:\n\nReviewer: ${rev.consumer?.displayName || "Customer"}\nTitle: ${rev.title}\nReview: ${(rev.text || "").slice(0, 500)}\n\nAcknowledge specific issue. Concrete next step. Under 80 words. Not defensive. Use their name.`,
      }),
    }).then((r) => r.json());

    console.log(`\n[${rev.stars}★] ${rev.title}`);
    console.log(`  Response: ${(gen.output || gen.content || gen.text || "").slice(0, 200)}`);
    await new Promise((r) => setTimeout(r, 500));
  }
  ```
</CodeGroup>

## Example Output

```text theme={"dark"}
[1★] Worst customer service ever
  Review: Waited 3 weeks for my order. Called 4 times. Each time I was told...
  Response: Hi Sarah, I'm genuinely sorry about the 3-week delay and the
  repeated calls without resolution. That's not the experience we want for
  anyone. I've flagged your order for priority attention — please email
  me directly at alex@company.com with your order number and I'll
  personally ensure this is resolved within 24 hours.

[2★] Product is fine but the website is terrible
  Review: I like the product but the checkout process crashed twice and...
  Response: Hi Mark, glad you like the product — and I hear you on the
  checkout issues. We're actively rebuilding the checkout flow this quarter.
  Your feedback accelerates that work. If you run into issues again,
  our team at support@company.com can process orders directly.
```

## Error Handling

<AccordionGroup>
  <Accordion title="Reply requires OAuth">Posting replies uses `POST /private/reviews/{id}/reply`, which requires OAuth 2.0 (not just API key). Set up the OAuth flow before automating responses.</Accordion>
  <Accordion title="One reply per review">Trustpilot allows one company reply per review. Attempting a second returns 409. Check `companyReply` field before responding.</Accordion>
  <Accordion title="Response length limit">Trustpilot limits replies to 10,000 characters. The 80-word prompt constraint keeps responses well under this limit.</Accordion>
  <Accordion title="Human review recommended">AI-generated responses should be reviewed by a human before posting. Queue responses for approval rather than auto-posting.</Accordion>
</AccordionGroup>
