A build log. No pitch, no fluff — just what I built, what broke, and whether it was worth it.
The Problem
I run a small agency. Every client birthday, I’d have the same internal debate: should I send something? A card felt right but took 20 minutes I didn’t have. An email felt cheap. So I’d do nothing — and then feel guilty when a competitor’s “Happy Birthday!” landed in the client’s inbox before mine.
The math was embarrassing. We had ~60 active clients. Remembering 60 birthdays manually is a systems failure waiting to happen. So I systemized it.
The Stack
Three pieces, all already in my life:
- HubSpot (our CRM) — already stores every client’s name, email, and birthday
- Greetu’s MCP server — exposes card creation, scheduling, replies, and reactions as callable tools
- A 40-line Python script — runs nightly, talks to both
No Zapier, no webhook gymnastics, no enterprise middleware.
The Build
Step 1: Get the API key
Grabbed a key from greetu.io/mcp —
grtu_... format, one header, done.Step 2: Export birthdays from the CRM
HubSpot exports birthdays as a contact property. One filtered view + weekly CSV export. The CSV has exactly the columns I need:
firstname, lastname, email, birthday.Step 3: The nightly script
The core loop is embarrassingly simple. Every night at 2am, a cron job runs this:
Python
import csv, requests
from datetime import datetime
API_KEY = "grtu_xxxxxxxx"
MCP = "https://greetu.io/api/mcp"
def mcp_call(method, params=None):
return requests.post(MCP, headers={
"Content-Type": "application/json",
"X-API-Key": API_KEY,
}, json={"jsonrpc": "2.0", "id": 1, "method": method,
"params": params or {}}).json()
today = datetime.now().strftime("%m-%d")
with open("birthdays.csv") as f:
for row in csv.DictReader(f):
if row["birthday"][5:] == today: # MM-DD match
mcp_call("tools/call", {
"name": "greetu.create_card",
"arguments": {
"recipient_name": row["firstname"],
"occasion": "birthday",
"use_ai_message": True,
"sender_name": "Our team at [Agency]",
}
})Every client with a birthday today gets a card with an AI-written, personalized message. Unique per person — not a template.
Step 4: The weekly human pass
This part matters. Every Monday morning, I skim the week’s queued cards via
greetu.list_cards. Takes ~5 minutes for a week of cards. I edit any message that feels off, add a voice note for VIP clients (yes, the MCP accepts Base64 audio), and let the rest fire.Step 5: Closing the loop
The part I didn’t expect to love:
greetu.get_replies and greetu.get_reactions. When a client opens their card and reacts or replies, it comes back through the same MCP endpoint. That feedback now feeds a simple Slack digest. The team sees “🎉 Maria reacted ❤️ to her card” in real time. Morale boost, unexpected.What Broke
Honest section, because build logs without failure are fiction:
- Date formats. My CRM exported
MM/DD/YYYY, my script expected ISO. Cards silently didn’t fire for two test runs. Fixed with one strftime change. - Duplicate sends. Cron ran at 2am; my laptop slept. Missed runs piled into a catch-up burst that double-sent two cards. Solution: a tiny
sent_log.jsondedupe. - AI tone. Early AI messages were… fine. Generic-fine. Adding
sender_nameand more CRM context (industry, past project notes) to the arguments made messages noticeably warmer. Context in, warmth out.
Was It Worth It?
Sixty seconds of honesty: yes, and it wasn’t close.
Setup took one evening. Ongoing cost is the Monday 5-minute review. And the results are the kind that don’t show up neatly in a dashboard — a client calling to say “that card made my day,” a reply on a card from someone we hadn’t heard from in months, a renewal conversation that started with “by the way, loved the birthday thing.”
Retention economics say a retained client is worth far more than the cost of a card. But honestly? I’d keep it running just for the replies.
Try It
The whole stack is smaller than it sounds: a CSV, a key, and a nightly script. If you can write a
for loop, you can build this.👉 Grab a Greetu API key and see the MCP docs: greetu.io/mcp — the
create_card, schedule_card, and get_replies tools I used are all documented there, free to start.