Docs / Guides

Complete Guide

Battle Card API: Generate and Maintain Competitive Battle Cards at Scale

How to build always-up-to-date competitive battle cards powered by AI. From first API call to production integrations with your CRM, Slack, and AI agents.

Updated March 2026 · 20 min read

What are competitive battle cards and why do sales teams need them?

A competitive battle card is a concise reference document that arms sales reps with everything they need to win against a specific competitor. It is the difference between an account executive stumbling through a competitive question on a call and confidently reframing the conversation in your favour.

The best battle cards are not marketing fluff. They are honest, tactical documents that acknowledge where competitors are strong, identify where they are weak, and provide specific talk tracks for the most common objections your reps face in the field.

71%

of reps say they lack adequate competitive information during sales calls (SiriusDecisions)

23%

higher win rate for teams with up-to-date competitive battle cards (Crayon)

89%

of competitive deals involve the prospect mentioning a specific competitor by name

When a prospect says "We are also looking at Acme Corp," the rep who can immediately speak to Acme's pricing model, known limitations, and differentiators wins. The rep who says "Let me get back to you on that" has already lost momentum.

Battle cards solve this by putting competitive intelligence in a format that is quick to scan, easy to memorize, and directly applicable to sales conversations. But there is a catch: they only work when they are current.

The problem: Battle cards go stale within weeks

Most companies create battle cards as a one-time project. A product marketing manager spends a week researching each competitor, writes detailed cards, uploads them to the wiki, announces them in Slack, and moves on to the next project.

Within weeks, those cards start to decay:

01

Competitor updates pricing — your card still shows the old numbers

A rep on a call quotes the competitor's price from two months ago. The prospect corrects them. Credibility evaporates. The rep now questions whether any of the competitive information they have been given is reliable.

02

Competitor ships a feature you listed as a weakness

Your battle card says "They lack SSO support." The competitor shipped SSO three weeks ago. Your rep confidently makes this claim. The prospect, who just saw the competitor's SSO demo, now trusts you less than the other vendor.

03

Competitor changes messaging — your positioning is off

Your battle card positions you against a "developer tool for startups." The competitor has repositioned as an "enterprise platform." Your reps are fighting last quarter's battle, not this quarter's.

04

Nobody owns the update cycle

The PMM who created the cards moved to another project. Nobody is assigned to update them. Months pass. New reps join and receive outdated materials. The battle cards become a historical artifact rather than a living sales tool.

The data

According to Crayon's 2025 State of Competitive Intelligence report, the average battle card is updated only 1.7 times per year. In a market where competitors ship updates monthly and change pricing quarterly, a battle card that is reviewed every six months is a liability, not an asset.

AI-powered battle cards that update automatically

The fundamental insight is that battle cards should not be static documents — they should be generated artifacts that reflect the current state of competitive intelligence. When competitor data changes, the battle card should update.

RivalCheck's battle card system works in three stages:

COLLECT

Continuous monitoring gathers competitive data

RivalCheck monitors competitor pages on a schedule — pricing, features, about, and any custom pages you add. Every change is detected, categorised, and stored with AI analysis. This creates a rich, structured dataset about each competitor that grows over time.

SYNTHESIZE

AI generates structured battle cards from raw intelligence

When you request a battle card — via the dashboard or API — RivalCheck's AI processes all available competitive data: current page content, change history, pricing information, and detected positioning. It produces a structured document with standardised sections that sales teams can immediately use.

REFRESH

Cards regenerate when significant changes are detected

When RivalCheck detects a high-severity change — a pricing update, new feature launch, or major messaging shift — the battle card is flagged as stale. You can configure automatic regeneration via webhooks, or trigger it manually through the API. The card always reflects the latest intelligence.

The result is a battle card that is never more than one monitoring cycle behind reality. Instead of a PMM spending 4-8 hours per competitor per quarter manually updating cards, the system keeps them current automatically.

Battle card anatomy: What each section contains

RivalCheck battle cards follow a standardised structure designed for quick scanning during sales calls. Each section serves a specific purpose in the competitive conversation:

OVERVIEW

Competitor overview

A 2-3 sentence summary of who the competitor is, what they do, and how they position themselves. This is the 30-second context a rep needs before jumping into a call where this competitor comes up unexpectedly.

STRENGTHS

Their strengths

An honest assessment of where the competitor is strong. This is not about being defeatist — it is about being credible. A rep who acknowledges a competitor's genuine strength and then reframes the conversation earns more trust than one who pretends competitors have no redeeming qualities.

WEAKNESSES

Their weaknesses

Known limitations, gaps in functionality, common customer complaints, and areas where they are behind. These are the wedges your sales team can use — but only if they are current. A weakness that was fixed last month becomes a credibility trap.

PRICING

Pricing comparison

A side-by-side view of their pricing tiers versus yours. Includes plan names, prices, key feature inclusions, and any notable differences in pricing model (per-seat vs. per-usage, monthly vs. annual-only, etc.). Updated automatically when pricing changes are detected.

HOW WE WIN

How we win against them

Specific differentiators and talk tracks for positioning your product favourably. Not generic claims — concrete examples like "We include SSO on all plans, they gate it behind Enterprise" or "Our average onboarding time is 2 hours vs. their 2-week implementation."

OBJECTIONS

Objection handling

The 3-5 most common objections prospects raise when comparing you to this competitor, with recommended responses. These are generated from competitive positioning analysis and refined over time as the AI observes how the competitor positions against your category.

ELEVATOR

Elevator pitch

A 2-3 sentence pitch that a rep can use when a prospect asks "How are you different from [Competitor]?" This is the single most useful piece of the battle card — the answer to the question that comes up in every competitive deal.

Using the Battle Card API

The Battle Card API provides two endpoints: one to retrieve the current card, and one to trigger regeneration. Both are scoped to a specific competitor.

GET /api/v1/competitors/:id/battle_card

Retrieve the current battle card for a competitor. Returns the most recently generated card with metadata about freshness.

Parameters

format Response format: json (default), markdown, or structured

Example request:

curl -s -H "Authorization: Bearer $RIVALCHECK_API_KEY" \
  "https://app.rivalcheck.com/api/v1/competitors/42/battle_card"

Full JSON response:

{
  "battle_card": {
    "competitor_id": 42,
    "competitor_name": "Acme Corp",
    "generated_at": "2026-03-22T10:30:00Z",
    "stale": false,
    "staleness_reason": null,
    "sections": {
      "overview": "Acme Corp is a project management platform targeting
        mid-market B2B companies. They position themselves as the 'all-in-one
        workspace' with strong emphasis on customization and workflow
        automation. Founded in 2019, they have raised $45M and serve
        approximately 2,000 customers.",

      "strengths": [
        "Highly customizable workflow engine with 200+ templates",
        "Strong brand recognition in the project management space",
        "Native integrations with 40+ tools including Jira, Linear, GitHub",
        "Generous free tier attracts developers who later advocate internally"
      ],

      "weaknesses": [
        "Pricing recently increased 17% on Enterprise tier, causing churn",
        "No native time tracking — requires third-party integration",
        "Mobile app rated 3.2 stars, frequently cited as unreliable",
        "Complex permissions model frustrates administrators",
        "Onboarding requires professional services for enterprise deployments"
      ],

      "pricing_comparison": {
        "their_plans": [
          { "name": "Starter", "price": "$29/mo", "key_features":
            "5 users, basic workflows, 10GB storage" },
          { "name": "Pro", "price": "$79/mo", "key_features":
            "25 users, advanced workflows, API access, 100GB storage" },
          { "name": "Enterprise", "price": "$349/mo", "key_features":
            "Unlimited users, SSO, custom integrations, SLA" }
        ],
        "key_differences": [
          "We include API access on all plans; they gate it behind Pro",
          "Their Enterprise is 40% more expensive for comparable features",
          "They charge per-workspace; we charge per-organisation (simpler)"
        ],
        "last_pricing_change": "2026-03-22 — Enterprise raised from $299 to $349"
      },

      "how_we_win": [
        "Speed to value: Average setup time is 15 minutes vs their 2-day
          onboarding process for teams over 10 people",
        "Transparent pricing: No hidden per-seat costs or workspace fees",
        "AI-native: Our AI features are built-in, not bolted on.
          Theirs requires a separate add-on at additional cost",
        "Customer success: We assign a dedicated CSM at Pro tier;
          they require Enterprise for dedicated support"
      ],

      "objection_handling": [
        {
          "objection": "Acme has more integrations than you",
          "response": "Acme lists 40+ integrations, but most are shallow
            data syncs. We focus on deep, two-way integrations with the
            tools your team actually uses. Which integrations are most
            important to your workflow? Let me show you how ours work."
        },
        {
          "objection": "We already use Acme and switching costs are high",
          "response": "We offer a free migration service and can run in
            parallel during the transition. Most teams are fully migrated
            in under a week. The question is whether the ongoing cost of
            Acme's limitations outweighs a short migration period."
        },
        {
          "objection": "Acme has more customers / bigger brand",
          "response": "They have been around longer, absolutely. But
            companies our size are switching because their recent price
            increases and slower innovation cycle suggest they are
            optimizing for margins, not customer value. I can share
            three customer stories from companies who made the switch."
        }
      ],

      "elevator_pitch": "Unlike Acme Corp, which has grown complex and
        expensive as they chase enterprise deals, we are purpose-built for
        teams that want powerful competitive intelligence without the
        overhead. Our AI-native approach means you get strategic insights
        from day one, not after a two-week implementation."
    }
  }
}

Markdown format:

Pass ?format=markdown to get the battle card as formatted Markdown — ideal for pasting into Notion, Confluence, or Slack:

curl -s -H "Authorization: Bearer $RIVALCHECK_API_KEY" \
  "https://app.rivalcheck.com/api/v1/competitors/42/battle_card?format=markdown"
{
  "battle_card": {
    "competitor_id": 42,
    "competitor_name": "Acme Corp",
    "generated_at": "2026-03-22T10:30:00Z",
    "stale": false,
    "format": "markdown",
    "content": "# Battle Card: Acme Corp\n\n## Overview\nAcme Corp is a project
      management platform targeting mid-market B2B companies...\n\n## Strengths
      \n- Highly customizable workflow engine with 200+ templates\n- Strong brand
      recognition...\n\n## Weaknesses\n- Pricing recently increased 17%...\n\n
      ## Pricing Comparison\n| Plan | Their Price | Our Price |\n|------|...\n\n
      ## How We Win\n1. Speed to value...\n\n## Common Objections\n### \"Acme
      has more integrations\"\n...\n\n## Elevator Pitch\n> Unlike Acme Corp..."
  }
}

Structured format:

The structured format returns each section as a separate keyed object with both raw content and metadata, useful for programmatic processing:

curl -s -H "Authorization: Bearer $RIVALCHECK_API_KEY" \
  "https://app.rivalcheck.com/api/v1/competitors/42/battle_card?format=structured"
POST /api/v1/competitors/:id/battle_card/generate

Trigger battle card regeneration. The card is generated asynchronously — the endpoint returns a job status, and the new card is available within 30-60 seconds.

Request Body (optional)

force Regenerate even if the card is not stale (default: false)

Example:

curl -X POST -H "Authorization: Bearer $RIVALCHECK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"force": true}' \
  "https://app.rivalcheck.com/api/v1/competitors/42/battle_card/generate"

Response:

{
  "status": "generating",
  "message": "Battle card generation started. Check back in 30-60 seconds.",
  "competitor_id": 42,
  "estimated_completion": "2026-03-24T15:01:00Z"
}

Integration patterns

Battle cards are most valuable when they are accessible in the tools your team already uses. Here are five integration patterns, from simplest to most sophisticated.

Slack bot: Battle cards on demand

Build a Slack slash command that lets reps pull up battle cards instantly. When a rep types /battlecard Acme Corp, they get the key sections right in Slack.

# Slack slash command handler (Python + Flask)
from flask import Flask, request, jsonify
import requests

app = Flask(__name__)
RIVALCHECK_API = "https://app.rivalcheck.com/api/v1"
API_KEY = os.environ["RIVALCHECK_API_KEY"]

@app.route("/slack/battlecard", methods=["POST"])
def battlecard_command():
    competitor_name = request.form.get("text", "").strip()

    # Find the competitor by name
    resp = requests.get(
        f"{RIVALCHECK_API}/competitors",
        headers={"Authorization": f"Bearer {API_KEY}"}
    )
    competitors = resp.json()["competitors"]
    match = next(
        (c for c in competitors
         if competitor_name.lower() in c["name"].lower()),
        None
    )

    if not match:
        return jsonify({
            "response_type": "ephemeral",
            "text": f"No competitor found matching '{competitor_name}'."
        })

    # Fetch the battle card in markdown format
    card_resp = requests.get(
        f"{RIVALCHECK_API}/competitors/{match['id']}/battle_card?format=markdown",
        headers={"Authorization": f"Bearer {API_KEY}"}
    )
    card = card_resp.json()["battle_card"]

    # Format for Slack
    sections = card["sections"]
    blocks = [
        {
            "type": "header",
            "text": {"type": "plain_text",
                     "text": f"Battle Card: {match['name']}"}
        },
        {
            "type": "section",
            "text": {"type": "mrkdwn",
                     "text": f"*Overview:* {sections['overview']}"}
        },
        {"type": "divider"},
        {
            "type": "section",
            "text": {"type": "mrkdwn",
                     "text": "*How We Win:*\n" + "\n".join(
                         f"- {point}" for point in sections["how_we_win"]
                     )}
        },
        {"type": "divider"},
        {
            "type": "section",
            "text": {"type": "mrkdwn",
                     "text": f"*Elevator Pitch:*\n>{sections['elevator_pitch']}"}
        },
        {
            "type": "context",
            "elements": [{
                "type": "mrkdwn",
                "text": f"Generated {card['generated_at'][:10]} "
                        f"| {'Stale' if card['stale'] else 'Current'} "
                        f"| "
            }]
        }
    ]

    return jsonify({
        "response_type": "in_channel",
        "blocks": blocks
    })

CRM integration: Salesforce and HubSpot

Sync battle card data into your CRM so reps see competitive intelligence right on the opportunity record. This example updates a custom Salesforce object linked to the Account:

# Sync battle cards to Salesforce custom object
from simple_salesforce import Salesforce
import requests

def sync_battle_cards_to_salesforce():
    """Sync all battle cards to Salesforce Competitive_Card__c records."""
    sf = Salesforce(username="...", password="...", security_token="...")
    headers = {"Authorization": f"Bearer {API_KEY}"}

    # Get all competitors with battle cards
    resp = requests.get(f"{RIVALCHECK_API}/competitors", headers=headers)
    competitors = resp.json()["competitors"]

    for comp in competitors:
        if not comp.get("has_battle_card"):
            continue

        # Fetch the battle card
        card_resp = requests.get(
            f"{RIVALCHECK_API}/competitors/{comp['id']}/battle_card",
            headers=headers
        )
        card = card_resp.json()["battle_card"]
        sections = card["sections"]

        # Upsert to Salesforce
        sf.Competitive_Card__c.upsert(
            f"RivalCheck_ID__c/{comp['id']}",
            {
                "Name": comp["name"],
                "Overview__c": sections["overview"],
                "Strengths__c": "\n".join(f"- {s}" for s in sections["strengths"]),
                "Weaknesses__c": "\n".join(f"- {w}" for w in sections["weaknesses"]),
                "How_We_Win__c": "\n".join(f"- {h}" for h in sections["how_we_win"]),
                "Elevator_Pitch__c": sections["elevator_pitch"],
                "Pricing_Notes__c": "\n".join(
                    sections["pricing_comparison"]["key_differences"]
                ),
                "Last_Updated__c": card["generated_at"],
                "Is_Stale__c": card["stale"]
            }
        )

    print(f"Synced {len(competitors)} battle cards to Salesforce")

For HubSpot, use their custom objects API with a similar pattern. The battle card data structure maps cleanly to any CRM's custom fields.

Notion and Confluence sync

Keep a "Competitive Intelligence" page in your team wiki that auto-updates with the latest battle cards. Use the Markdown format for easy syncing:

# Sync battle cards to Notion database
from notion_client import Client
import requests

notion = Client(auth=os.environ["NOTION_TOKEN"])
DATABASE_ID = "your-database-id"

def sync_to_notion():
    headers = {"Authorization": f"Bearer {API_KEY}"}

    # Get all competitors
    resp = requests.get(f"{RIVALCHECK_API}/competitors", headers=headers)
    competitors = resp.json()["competitors"]

    for comp in competitors:
        if not comp.get("has_battle_card"):
            continue

        # Fetch markdown format
        card_resp = requests.get(
            f"{RIVALCHECK_API}/competitors/{comp['id']}/battle_card"
            f"?format=markdown",
            headers=headers
        )
        card = card_resp.json()["battle_card"]

        # Check if page exists in Notion
        existing = notion.databases.query(
            database_id=DATABASE_ID,
            filter={"property": "Competitor",
                    "title": {"equals": comp["name"]}}
        )

        page_content = [
            {"type": "paragraph", "paragraph": {
                "rich_text": [{"type": "text",
                               "text": {"content": card["content"]}}]
            }}
        ]

        if existing["results"]:
            # Update existing page
            page_id = existing["results"][0]["id"]
            # Clear and rewrite content blocks
            notion.blocks.children.append(page_id, children=page_content)
        else:
            # Create new page
            notion.pages.create(
                parent={"database_id": DATABASE_ID},
                properties={
                    "Competitor": {"title": [{"text": {"content": comp["name"]}}]},
                    "Status": {"select": {"name": "Current" if not card["stale"]
                                          else "Stale"}},
                    "Last Updated": {"date": {"start": card["generated_at"][:10]}}
                },
                children=page_content
            )

Sales deck auto-population

Use the API to populate competitive slides in your sales deck. Pull the "how we win" and pricing comparison sections to generate a custom competitive slide for each deal:

# Generate a competitive slide using python-pptx
from pptx import Presentation
from pptx.util import Inches, Pt

def create_competitive_slide(competitor_id):
    headers = {"Authorization": f"Bearer {API_KEY}"}
    card_resp = requests.get(
        f"{RIVALCHECK_API}/competitors/{competitor_id}/battle_card",
        headers=headers
    )
    card = card_resp.json()["battle_card"]["sections"]

    prs = Presentation("template.pptx")
    slide = prs.slides.add_slide(prs.slide_layouts[1])

    slide.shapes.title.text = f"Why Us vs {card_resp.json()['battle_card']['competitor_name']}"

    body = slide.placeholders[1]
    tf = body.text_frame
    tf.clear()

    for point in card["how_we_win"]:
        p = tf.add_paragraph()
        p.text = point
        p.font.size = Pt(14)
        p.level = 0

    prs.save(f"competitive_slide_{competitor_id}.pptx")
    return f"competitive_slide_{competitor_id}.pptx"

MCP: Conversational battle cards via Claude

With the RivalCheck MCP server connected, your team can ask Claude for battle card information naturally:

You:

"Get me the battle card for Acme Corp. Focus on how their recent pricing increase affects our competitive positioning."

Claude (via MCP):

Retrieves the battle card and recent changes, then generates a tailored briefing that combines the standard battle card with analysis of the specific pricing change.

You:

"Write me 3 email subject lines I can use to reach out to Acme's customers about this price increase."

This pattern makes battle cards conversational. Instead of reading a document, reps can ask questions and get tailored competitive advice in real time.

Keeping battle cards fresh

The whole point of API-powered battle cards is that they stay current. Here are three strategies for ensuring your cards never go stale:

Webhook-triggered regeneration

Configure a webhook to listen for change.detected events. When a high-severity change is detected, automatically regenerate the affected competitor's battle card:

@app.route("/webhooks/rivalcheck", methods=["POST"])
def auto_regenerate_battle_card():
    event = request.json

    if event["event"] != "change.detected":
        return "OK", 200

    change = event["data"]

    # Only regenerate for high-severity changes
    if change["severity"] != "high":
        return "OK", 200

    competitor_id = change["competitor"]["id"]

    # Trigger battle card regeneration
    requests.post(
        f"{RIVALCHECK_API}/competitors/{competitor_id}/battle_card/generate",
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={"force": True}
    )

    # Notify the team
    slack_webhook = os.environ["SLACK_WEBHOOK_URL"]
    requests.post(slack_webhook, json={
        "text": f"Battle card for {change['competitor']['name']} is being "
                f"regenerated due to: {change['title']}"
    })

    return "OK", 200

Scheduled regeneration

Even without specific changes, regenerate all battle cards on a schedule (e.g., weekly) to incorporate any cumulative updates. A simple cron job or GitHub Action handles this:

# .github/workflows/regenerate-battle-cards.yml
name: Regenerate Battle Cards
on:
  schedule:
    - cron: '0 8 * * 1'  # Every Monday at 8 AM UTC

jobs:
  regenerate:
    runs-on: ubuntu-latest
    steps:
      - name: Regenerate all battle cards
        env:
          RIVALCHECK_API_KEY: ${{ secrets.RIVALCHECK_API_KEY }}
        run: |
          # Get all active competitors
          COMPETITORS=$(curl -s \
            -H "Authorization: Bearer $RIVALCHECK_API_KEY" \
            "https://app.rivalcheck.com/api/v1/competitors?status=active")

          # Regenerate each battle card
          echo "$COMPETITORS" | jq -r '.competitors[].id' | while read id; do
            echo "Regenerating battle card for competitor $id..."
            curl -X POST \
              -H "Authorization: Bearer $RIVALCHECK_API_KEY" \
              -H "Content-Type: application/json" \
              -d '{"force": true}' \
              "https://app.rivalcheck.com/api/v1/competitors/$id/battle_card/generate"
            sleep 2  # Rate limiting courtesy
          done

Staleness detection

Every battle card API response includes a stale boolean and an optional staleness_reason. Use these to flag cards that need attention:

# Check all battle cards for staleness
def audit_battle_card_freshness():
    headers = {"Authorization": f"Bearer {API_KEY}"}
    resp = requests.get(f"{RIVALCHECK_API}/competitors", headers=headers)
    stale_cards = []

    for comp in resp.json()["competitors"]:
        if not comp.get("has_battle_card"):
            continue

        card = requests.get(
            f"{RIVALCHECK_API}/competitors/{comp['id']}/battle_card",
            headers=headers
        ).json()["battle_card"]

        if card["stale"]:
            stale_cards.append({
                "competitor": comp["name"],
                "reason": card["staleness_reason"],
                "last_generated": card["generated_at"]
            })

    if stale_cards:
        # Alert the team
        message = "Stale battle cards detected:\n"
        for card in stale_cards:
            message += (f"- {card['competitor']}: {card['reason']} "
                       f"(last updated {card['last_generated'][:10]})\n")
        send_slack_alert(message)

    return stale_cards

Building a battle card workflow: End-to-end example

Here is a complete Python application that ties together all the patterns above: it listens for webhook events, regenerates battle cards when changes are detected, syncs them to Slack and your CRM, and sends a weekly summary to the sales enablement team.

#!/usr/bin/env python3
"""
Battle Card Automation Service
Listens for RivalCheck webhooks, regenerates battle cards,
and syncs them to Slack and Salesforce.
"""

import os
import time
import logging
import requests
from flask import Flask, request, jsonify
from datetime import datetime

app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("battlecard-service")

RIVALCHECK_API = "https://app.rivalcheck.com/api/v1"
API_KEY = os.environ["RIVALCHECK_API_KEY"]
SLACK_WEBHOOK = os.environ["SLACK_WEBHOOK_URL"]
RC_HEADERS = {"Authorization": f"Bearer {API_KEY}"}


def regenerate_and_wait(competitor_id, max_wait=90):
    """Trigger regeneration and wait for completion."""
    # Trigger regeneration
    requests.post(
        f"{RIVALCHECK_API}/competitors/{competitor_id}/battle_card/generate",
        headers=RC_HEADERS,
        json={"force": True}
    )

    # Poll until complete (card is no longer stale)
    for _ in range(max_wait // 5):
        time.sleep(5)
        resp = requests.get(
            f"{RIVALCHECK_API}/competitors/{competitor_id}/battle_card",
            headers=RC_HEADERS
        )
        card = resp.json()["battle_card"]
        if not card["stale"]:
            return card

    logger.warning(f"Battle card generation timed out for competitor {competitor_id}")
    return None


def notify_slack(competitor_name, change_title, card):
    """Post battle card update notification to Slack."""
    sections = card["sections"]
    payload = {
        "blocks": [
            {
                "type": "header",
                "text": {
                    "type": "plain_text",
                    "text": f"Battle Card Updated: {competitor_name}"
                }
            },
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f"*Triggered by:* {change_title}\n\n"
                            f"*Key Updates:*\n"
                            f"- Strengths: {len(sections['strengths'])} items\n"
                            f"- Weaknesses: {len(sections['weaknesses'])} items\n"
                            f"- Objections: {len(sections['objection_handling'])} handled"
                }
            },
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f"*Elevator Pitch:*\n>{sections['elevator_pitch']}"
                }
            },
            {
                "type": "actions",
                "elements": [{
                    "type": "button",
                    "text": {"type": "plain_text", "text": "View Full Card"},
                    "url": f"https://app.rivalcheck.com/app/competitors/"
                           f"{card['competitor_id']}/battle_card"
                }]
            }
        ]
    }
    requests.post(SLACK_WEBHOOK, json=payload)


@app.route("/webhooks/rivalcheck", methods=["POST"])
def handle_change():
    """Handle incoming RivalCheck webhook events."""
    event = request.json
    logger.info(f"Received webhook: {event['event']}")

    if event["event"] != "change.detected":
        return "OK", 200

    change = event["data"]
    competitor_id = change["competitor"]["id"]
    competitor_name = change["competitor"]["name"]

    # Only auto-regenerate for high/medium severity
    if change["severity"] not in ("high", "medium"):
        logger.info(f"Skipping low-severity change for {competitor_name}")
        return "OK", 200

    logger.info(f"Regenerating battle card for {competitor_name}")

    # Regenerate the battle card
    card = regenerate_and_wait(competitor_id)

    if card:
        # Notify Slack
        notify_slack(competitor_name, change["title"], card)
        logger.info(f"Battle card updated and Slack notified for {competitor_name}")
    else:
        # Notify about failure
        requests.post(SLACK_WEBHOOK, json={
            "text": f"Failed to regenerate battle card for {competitor_name}. "
                    f"Please check manually."
        })

    return "OK", 200


@app.route("/health", methods=["GET"])
def health():
    return jsonify({"status": "ok", "service": "battlecard-automation"})


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

Deploy this as a service

This Flask app works well on Railway, Render, Fly.io, or any container platform. Set your RIVALCHECK_API_KEY and SLACK_WEBHOOK_URL as environment variables, then point your RivalCheck webhook settings at the /webhooks/rivalcheck endpoint.

Getting started with RivalCheck battle cards

RivalCheck generates AI-powered battle cards from the competitive data it collects through automated monitoring. Every plan includes battle card generation — no additional setup required beyond adding competitors.

Get your first battle card in 10 minutes:

  1. 1. Create your free account — 14-day trial, no credit card
  2. 2. Add a competitor URL. RivalCheck discovers key pages and starts monitoring within minutes.
  3. 3. Once the first scan completes, click "Generate Battle Card" on the competitor page, or use the API.
  4. 4. Share it with your sales team via Slack, your CRM, or the MCP server for AI-powered access.