Build Bonzo CRM drip campaigns programmatically via internal API. Creates campaigns, adds days with correct day numbers, adds SMS/Email events, and fills content. No browser automation needed. Use when asked to create drip campaigns, build campaign sequences, or automate Bonzo campaign creation.
Resources
3Install
npx skillscat add zacdcook/bonzo-campaign-builder Install via the SkillsCat registry.
Bonzo Campaign Builder — Internal API Automation
Build Bonzo CRM drip campaigns programmatically using the internal platform API. This approach is 100x faster than browser automation and uses pure curl/Python.
Before modifying this skill, read Reference/skill-maintenance.md if it exists.
Prerequisites
- Fresh Bonzo session cookies (see Cookie Setup below)
- Campaign content in JSON format (see Data Format below)
- No manual steps required - campaigns can be created from scratch via API
Cookie Setup (MANDATORY FIRST STEP)
The internal API uses session cookies, not the V3 Bearer token. Cookies must be extracted from a logged-in Bonzo browser session.
Ask the user:
"I need fresh Bonzo session cookies to build campaigns. Please:
- Open Bonzo in Chrome (platform.getbonzo.com)
- Open DevTools (F12) > Application tab > Cookies > platform.getbonzo.com
- Copy the values for
XSRF-TOKENandgetbonzo_session- Paste them both here."
Cookie lifetime: Valid for the browser session duration. Re-extract if you get 419 (CSRF mismatch) or 401 (Unauthenticated) errors.
API Endpoints (All Verified 2026-04-03)
All endpoints use https://platform.getbonzo.com/api/ base URL.
Both /api/ and /proxy/api/ prefixes work identically.
| Operation | Method | Endpoint | Payload |
|---|---|---|---|
| Create campaign shell | POST | /api/campaigns/store |
{"name": "...", "type": "consequtive"} |
| Initialize sequence | GET | /proxy/api/campaign/{campId}/new-sequence/ |
None (GET, no body) |
| Get sequence days | GET | /api/sequences/{seqId}/get-days |
None |
| Add day to sequence | POST | /api/sequences/{seqId}/add-day |
{} (empty, auto-increments) |
| Update day number | POST | /api/days/{dayId} |
{"day": N, "start_at": "09:00 am"} |
| Add event to day | POST | /api/sequences/add-event |
{"sequence": seqId, "day": dayId, "type": "sms"|"email"} |
| Save SMS content | POST | /api/sms/update-sequence/{eventId} |
{"message": "<p>text</p>", "event_name": "Untitled", "assigned_to": null, "gif_path": "", "image_type": ""} |
| Save Email content | POST | /api/email/update-sequence/{eventId} |
{"message": "html", "subject": "subj", "event_name": "Untitled", "assigned_to": null, "gif_path": "", "image_type": ""} |
| Get campaign details | GET | /v3/campaigns/{campId}/get |
None (uses Bearer token) |
Required Headers
Cookie: XSRF-TOKEN={xsrf}; getbonzo_session={session}
X-Xsrf-Token: {xsrf_url_decoded} ← Replace %3D with = in the XSRF value
Content-Type: application/json
Accept: application/json
Origin: https://platform.getbonzo.com
Referer: https://platform.getbonzo.com/campaigns/{campaignId}
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36Critical Details
start_atformat: Must beh:i aformat (e.g.,09:00 am). Using8:00 AMwill fail validation.start_atminimum: Must be>= 09:00 amper validation rules.- Add-day auto-increments: The day number auto-increments from the last day. You MUST call update-day after to set the correct number.
- Add-event payload: Uses
sequenceandday(NOTsequence_idandday_id). Wrong field names cause Server Error. - SMS message format: Wrap in
<p>tags:"<p>Your message here</p>" - Email message format: Full HTML, no wrapping needed.
- Cloudflare bot detection: Use curl (not Python urllib). Python urllib gets blocked with Error 1010. Add User-Agent header and 0.5-1s delays between requests.
- Cookie rotation: Bonzo rotates cookies on every response (Set-Cookie headers). But old cookies remain valid for a grace period. A single set of cookies works for an entire build session.
- Campaign name limit: Max 64 characters. Longer names fail with validation error.
- Two-step campaign creation:
POST /api/campaigns/storecreates the shell but does NOT create a sequence. You MUST also callGET /proxy/api/campaign/{id}/new-sequence/to initialize the sequence. Without this, the campaign has no sequence ID and you cannot add days or events. - Rate limiting: 0.3s between related calls (add-day, update-day, add-event, save-event), 0.5s between complete touches. Proven safe for building 70+ touches in one session without Cloudflare blocks.
- Batch by day: When a day has multiple events (e.g., Day 1 with SMS + Email + SMS), add the day once, then add all events to that day before moving to the next day. This is more efficient and avoids duplicate days.
How to Build a Campaign
Step 1: Create Campaign (or get existing)
To create a new campaign from scratch:
curl -s -X POST "https://platform.getbonzo.com/api/campaigns/store" \
-H "Cookie: XSRF-TOKEN=$XSRF; getbonzo_session=$SESSION" \
-H "X-Xsrf-Token: $XSRF_DECODED" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
-H "Origin: https://platform.getbonzo.com" \
-d '{"name": "Campaign Name Here", "type": "consequtive"}'Response includes data.id (campaign ID). Note: type is spelled consequtive (Bonzo's typo, must match). Max 64 characters for name.
Then initialize the sequence (REQUIRED - campaign shell has no sequence without this):
curl -s "https://platform.getbonzo.com/proxy/api/campaign/{campaignId}/new-sequence/" \
-H "Cookie: XSRF-TOKEN=$XSRF; getbonzo_session=$SESSION" \
-H "Accept: application/json" \
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"Then get the sequence ID:
source .env # BONZO_API_TOKEN
curl -s "https://app.getbonzo.com/api/v3/campaigns/{campaignId}/get" \
-H "Authorization: Bearer $BONZO_API_TOKEN" \
-H "Accept: application/json" | python -c "
import json, sys
d = json.load(sys.stdin)
c = d.get('campaign', d.get('data', {}))
print(f'Sequence ID: {c.get(\"sequence\", {}).get(\"id\", \"\")}')"Or for an existing campaign, just get the sequence ID with the same curl command.
Step 2: Parse Campaign Content
Parse the campaign markdown into JSON. Each touch needs:
{
"touch": 1,
"day": 10,
"type": "sms",
"body": "SMS text here"
}or for email:
{
"touch": 2,
"day": 14,
"type": "email",
"subject": "Email subject",
"html": "<p>HTML body</p>"
}Replace any placeholders in your content before building (e.g., application URLs, booking links).
Step 3: Run the Build Script
Use this Python pattern (MUST use curl subprocess, not urllib):
import json, subprocess, time, sys
XSRF = '{user-provided}'
SESSION = '{user-provided}'
XSRF_DECODED = XSRF.replace('%3D', '=')
SEQ_ID = {sequence_id}
BASE = 'https://platform.getbonzo.com/api'
UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
def api(method, path, data=None):
cmd = ['curl', '-s', '-X', method, f'{BASE}/{path}',
'-H', f'Cookie: XSRF-TOKEN={XSRF}; getbonzo_session={SESSION}',
'-H', f'X-Xsrf-Token: {XSRF_DECODED}',
'-H', 'Content-Type: application/json',
'-H', 'Accept: application/json',
'-H', f'User-Agent: {UA}',
'-H', 'Origin: https://platform.getbonzo.com',
'-H', 'Referer: https://platform.getbonzo.com/campaigns/{campaignId}']
if data:
cmd += ['-d', json.dumps(data)]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
return json.loads(result.stdout) if result.stdout.strip().startswith('{') else None
# For each touch:
for touch in touches:
# 1. Add day
r = api('POST', f'sequences/{SEQ_ID}/add-day', {})
day_id = r['day']['id']
time.sleep(0.5)
# 2. Set day number
api('POST', f'days/{day_id}', {'day': touch['day'], 'start_at': '09:00 am'})
time.sleep(0.5)
# 3. Add event
r = api('POST', 'sequences/add-event',
{'sequence': SEQ_ID, 'day': day_id, 'type': touch['type']})
event_id = r['id']
time.sleep(0.5)
# 4. Save content
if touch['type'] == 'sms':
api('POST', f'sms/update-sequence/{event_id}',
{'message': f'<p>{touch["body"]}</p>', 'event_name': 'Untitled',
'assigned_to': None, 'gif_path': '', 'image_type': ''})
else:
api('POST', f'email/update-sequence/{event_id}',
{'message': touch['html'], 'subject': touch['subject'],
'event_name': 'Untitled', 'assigned_to': None,
'gif_path': '', 'image_type': ''})
time.sleep(1) # Rate limitStep 4: Verify
curl -s "https://platform.getbonzo.com/api/sequences/{seqId}/get-days" \
-H "Cookie: XSRF-TOKEN=$XSRF; getbonzo_session=$SESSION" \
-H "Accept: application/json" | python -c "
import json, sys
data = json.load(sys.stdin)
print(f'Total days: {len(data)}')
for d in sorted(data, key=lambda x: x['day']):
events = d.get('events', [])
print(f' Day {d[\"day\"]:>3}: {len(events)} event(s)')
print(f'Total events: {sum(len(d.get(\"events\",[])) for d in data)}')"Scripts
| Script | Purpose |
|---|---|
scripts/build-campaign.py |
Create a campaign from a JSON content file. Handles full flow: create shell, init sequence, add days/events, save content. |
scripts/extract-cookies.py |
Extract Bonzo cookies from Chrome's local cookie database. Cross-platform (Win/Mac/Linux). |
Error Handling
| Error | Cause | Fix |
|---|---|---|
| 419 CSRF mismatch | XSRF token expired or not URL-decoded | Re-extract cookies; ensure %3D→= in X-Xsrf-Token header |
| 401 Unauthenticated | Session expired | Re-extract cookies from browser |
| 403 Cloudflare 1010 | Bot detection (Python urllib) | Use curl subprocess with User-Agent header |
| Server Error on add-event | Wrong field names | Use sequence and day (NOT sequence_id/day_id) |
| Validation error on start_at | Wrong time format | Use 09:00 am format (lowercase, h:i a) |
| "No query results" | Invalid day/event ID | Re-query get-days to get current IDs |
Changelog
| Date | Change |
|---|---|
| 2026-04-03 | Initial release: all 7 endpoints verified, build scripts, cookie extraction |
| 2026-04-04 | Generalized for public use: removed account-specific data, added generic build script |