Customer mention & comment alerts in Slack

When a customer writes a comment or @mentions your team in OnRamp, that's a hand going up. This recipe routes those moments into a Slack channel — with the message text, the task and project it belongs to, and whether the author is a customer — within seconds of it happening.

What you'll get

Unlike the at-risk radar (a daily pull), this one is push: OnRamp calls you the moment it happens, via a webhook. You run a small receiver — a few dozen lines — that turns each event into a Slack message. Plan on a few hours end to end.

How it works
  1. You subscribe to the comment_mention and comment_new events.
  2. OnRamp POSTs a JSON event to your receiver URL when they happen.
  3. Your receiver re-fetches the comment via the API, enriches it with task and author context, and posts to Slack.

Why re-fetch instead of trusting the event body? Webhook deliveries are not yet signed or authenticated, so anyone who learns your receiver URL could POST fake events at it. Treat the event as a doorbell, not a package: take only the uuid from it, and fetch the real data from the API with your key. This "fetch-on-event" habit is the single most important pattern in every webhook recipe.

Step 1 — Subscribe to the events
curl -X POST -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event_code": "comment_mention", "hook_url": "https://your-receiver.example.com/onramp-events"}' \
  "https://api.onramp.us/v1/webhooks"

curl -X POST -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event_code": "comment_new", "hook_url": "https://your-receiver.example.com/onramp-events"}' \
  "https://api.onramp.us/v1/webhooks"
import requests

BASE = "https://api.onramp.us/v1"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}

for event_code in ("comment_mention", "comment_new"):
    requests.post(f"{BASE}/webhooks", headers=HEADERS, json={
        "event_code": event_code,
        "hook_url": "https://your-receiver.example.com/onramp-events",
    })
for (const event_code of ["comment_mention", "comment_new"]) {
  await fetch("https://api.onramp.us/v1/webhooks", {
    method: "POST",
    headers: {
      Authorization: "Bearer YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      event_code,
      hook_url: "https://your-receiver.example.com/onramp-events",
    }),
  });
}

The full catalog of subscribable events is at GET /v1/events; every event's payload shape is in the webhook payloads reference.

Step 2 — Find your API user's identity (echo-loop insurance)

Comments your own scripts post through the API also fire comment_new. If you ever add a "reply from Slack" feature (the two-way bridge), an unfiltered receiver will alert on its own posts — and in the two-way case, loop forever. Build the filter now, while it's one line:

Post one comment via the API to any test task, read back its author, and save that UUID:

curl -X POST -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"message_plain": "identity probe — ignore"}' \
  "https://api.onramp.us/v1/tasks/TEST_TASK_UUID/comments"
# → note "created_by_uuid" in the response: that is YOUR api user. Save it.
Step 3 — The receiver

A ~50-line web service. Run it anywhere that gives you an HTTPS URL (Cloud Run, Lambda + API Gateway, Render, your own box).

from flask import Flask, request

import requests

BASE = "https://api.onramp.us/v1"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
SLACK_URL = "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK"
MY_API_USER_UUID = "…from step 2…"

app = Flask(__name__)
seen = set()  # deliveries can repeat — dedupe (swap for Redis/db in production)

def get(path):
    return requests.get(f"{BASE}{path}", headers=HEADERS).json()["data"]

@app.post("/onramp-events")
def on_event():
    event = request.get_json(silent=True) or {}
    comment_uuid = (event.get("payload") or {}).get("comment", {}).get("uuid")
    if not comment_uuid:
        return "", 204                       # not a comment event — ignore
    key = (event.get("event_name"), comment_uuid, event.get("timestamp"))
    if key in seen:
        return "", 204                       # duplicate delivery — ignore
    seen.add(key)

    # Fetch-on-event: trust only the uuid, pull the real data yourself
    comment = get(f"/comments/{comment_uuid}")
    if comment["created_by_uuid"] == MY_API_USER_UUID:
        return "", 204                       # our own API post — never echo

    task = get(f"/tasks/{comment['associated_object_uuid']}")
    kind = "🔔 Mention" if event.get("event_name") == "comment_mention" else "💬 New comment"
    requests.post(SLACK_URL, json={"text": (
        f"{kind} from *{comment['created_by_name']}* on "
        f"*{task['name']}* ({task['project']['name']})\n"
        f"> {comment['message_plain']}\n"
        f"<{task['internal_project_url']}|Open in OnRamp>"
    )})
    return "", 200

app.run(port=8080)
import express from "express";

const BASE = "https://api.onramp.us/v1";
const HEADERS = { Authorization: "Bearer YOUR_API_KEY" };
const SLACK_URL = "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK";
const MY_API_USER_UUID = "…from step 2…";

const get = async (path) =>
  (await (await fetch(BASE + path, { headers: HEADERS })).json()).data;

const seen = new Set(); // dedupe repeats (swap for Redis/db in production)
const app = express().use(express.json());

app.post("/onramp-events", async (req, res) => {
  const event = req.body ?? {};
  const commentUuid = event.payload?.comment?.uuid;
  if (!commentUuid) return res.sendStatus(204);
  const key = `${event.event_name}:${commentUuid}:${event.timestamp}`;
  if (seen.has(key)) return res.sendStatus(204);
  seen.add(key);

  const comment = await get(`/comments/${commentUuid}`);
  if (comment.created_by_uuid === MY_API_USER_UUID) return res.sendStatus(204);

  const task = await get(`/tasks/${comment.associated_object_uuid}`);
  const kind = event.event_name === "comment_mention" ? "🔔 Mention" : "💬 New comment";
  await fetch(SLACK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      text: `${kind} from *${comment.created_by_name}* on *${task.name}* (${task.project.name})\n> ${comment.message_plain}\n<${task.internal_project_url}|Open in OnRamp>`,
    }),
  });
  res.sendStatus(200);
});

app.listen(8080);
# The receiver is a small web service (see Python/Node tabs). These are the
# two enrichment calls it makes for each event:

# The real comment (author, text, which task it's on)
curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.onramp.us/v1/comments/COMMENT_UUID"

# The task, its project, and dashboard links
curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.onramp.us/v1/tasks/TASK_UUID"
Make it yours
  • Tag customer authors. Fetch GET /v1/users?is_customer_user=true once, cache the UUID set, and prefix alerts from customers with a CUSTOMER badge — a customer comment usually deserves faster eyes than an internal one.
  • Route by project. Map project UUIDs to different Slack channels so each implementation squad only sees its own accounts.
  • Only mentions. If comment_new is too chatty for your volume, subscribe to comment_mention alone and teach customers to @mention.
Good to know
  • Deliveries are at-least-once. The same event can arrive twice — that's why the receiver dedupes on (event_name, comment uuid, timestamp) before acting.
  • Answer fast, work after. Return 200 quickly (under ~10s) and do slow work afterwards if you add any; slow receivers get retried, which looks like duplicates.
  • A "help requested" event (task_help_requested) exists in the event catalog, but does not currently fire — we've flagged it. Comment mentions cover the "customer raising a hand" moment today; this guide will be updated when it goes live.
  • UUID-only. Integer IDs disappear from the API on July 31, 2026.