Request discount approval in Slack for HubSpot deals using code

high complexityCost: $0

Prerequisites

Prerequisites
  • Node.js or Python
  • HubSpot private app token (read + write scopes for deals)
  • Slack Bot Token with chat:write scope + interactive messages enabled
  • A server to handle Slack interactivity callbacks

Why code?

The code approach is the only option that gives you a full interactive approval loop with zero ongoing cost. You get real-time Slack buttons (Approve/Deny) that write decisions back to HubSpot instantly — no polling delay, no per-task fees. You pay only for hosting, which can be free on Vercel's serverless tier.

The trade-off is setup complexity. You need two endpoints: one to receive HubSpot webhook events and another to handle Slack button clicks. If your team has a developer comfortable with Express or Flask, this takes 45-60 minutes. If not, n8n gives you the same interactive approval experience with a visual builder.

How it works

  • HubSpot webhook pushes deal.propertyChange events for the discount_percent property to your server in real time
  • Webhook handler checks whether the discount exceeds your threshold and posts a Slack Block Kit message with Approve/Deny buttons
  • Slack interactivity endpoint receives button clicks, updates the HubSpot deal's discount_approval property, and updates the Slack message to show the decision
  • Rep notification sends a DM or thread reply to the rep with the approval or denial

Architecture

This requires two endpoints:

  1. Webhook receiver — receives HubSpot deal change events, checks discount threshold, sends Slack message with Approve/Deny buttons
  2. Interactivity handler — receives Slack button clicks, updates HubSpot deal, notifies the rep

Step 1: Send approval request with buttons

When a deal's discount exceeds the threshold, post a message with Block Kit interactive buttons:

# In your webhook handler after checking discount > 15%
slack.chat_postMessage(
    channel=os.environ["DEAL_DESK_CHANNEL"],
    text=f"Discount approval needed for {deal_name}",
    blocks=[
        {"type": "section", "text": {"type": "mrkdwn",
            "text": f"🏷️ *Discount Approval Needed*\n*Deal:* {deal_name}\n*Amount:* ${amount:,.0f}\n*Discount:* {discount}%"}},
        {"type": "actions", "elements": [
            {"type": "button", "text": {"type": "plain_text", "text": "✅ Approve"},
             "style": "primary", "action_id": "approve_discount",
             "value": json.dumps({"deal_id": deal_id, "discount": discount})},
            {"type": "button", "text": {"type": "plain_text", "text": "❌ Deny"},
             "style": "danger", "action_id": "deny_discount",
             "value": json.dumps({"deal_id": deal_id})}
        ]}
    ]
)

Step 2: Handle the button click

Set up a Slack interactivity endpoint (configure in your Slack app under Interactivity & Shortcuts):

@app.route("/slack/interact", methods=["POST"])
def handle_interaction():
    payload = json.loads(request.form["payload"])
    action = payload["actions"][0]
    data = json.loads(action["value"])
    user = payload["user"]["name"]
 
    approved = action["action_id"] == "approve_discount"
    status = "Approved" if approved else "Denied"
 
    # Update HubSpot
    requests.patch(
        f"https://api.hubapi.com/crm/v3/objects/deals/{data['deal_id']}",
        headers={**HEADERS, "Content-Type": "application/json"},
        json={"properties": {"discount_approval": status}}
    )
 
    # Update the Slack message to show the decision
    slack.chat_update(
        channel=payload["channel"]["id"],
        ts=payload["message"]["ts"],
        text=f"Discount {'approved' if approved else 'denied'} by {user}",
        blocks=[
            {"type": "section", "text": {"type": "mrkdwn",
                "text": f"{'✅' if approved else '❌'} Discount *{status}* by {user}"}}
        ]
    )
    return "", 200
Slack interactivity setup

You must configure your Slack app's Request URL under Interactivity & Shortcuts to point to your server's /slack/interact endpoint. Without this, button clicks do nothing.

Troubleshooting

Common questions

How do I restrict who can click the Approve button?

Add a check in your interactivity handler that compares payload.user.id against a list of authorized manager Slack IDs. If an unauthorized user clicks, respond with an ephemeral message ("You don't have permission to approve discounts") and don't update HubSpot.

Do I need to verify HubSpot webhook signatures?

Yes, in production. HubSpot signs webhook payloads with your app's client secret. Verify the X-HubSpot-Signature-v3 header to ensure requests actually come from HubSpot. Without this, anyone who discovers your endpoint URL could send fake discount events.

What happens if my server is down when a discount is applied?

HubSpot retries failed webhook deliveries for up to 24 hours. When your server comes back online, it will receive the queued events. If you need guaranteed delivery, add a message queue (SQS, Redis) between the webhook receiver and the processing logic.

Can I deploy this on a serverless platform like Vercel or AWS Lambda?

Yes. The webhook receiver works well as a serverless function. The Slack interactivity handler also works, but you need to respond within 3 seconds — acknowledge the request immediately with a 200 response and process the HubSpot update asynchronously.

Cost

  • Free — hosting costs only. No per-execution fees.

Looking to scale your AI operations?

We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.