zacdcook

bonzo-campaign-builder

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.

zacdcook 0 Updated 2mo ago

Resources

3
GitHub

Install

npx skillscat add zacdcook/bonzo-campaign-builder

Install via the SkillsCat registry.

SKILL.md

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

  1. Fresh Bonzo session cookies (see Cookie Setup below)
  2. Campaign content in JSON format (see Data Format below)
  3. 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:

  1. Open Bonzo in Chrome (platform.getbonzo.com)
  2. Open DevTools (F12) > Application tab > Cookies > platform.getbonzo.com
  3. Copy the values for XSRF-TOKEN and getbonzo_session
  4. 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.36

Critical Details

  • start_at format: Must be h:i a format (e.g., 09:00 am). Using 8:00 AM will fail validation.
  • start_at minimum: Must be >= 09:00 am per 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 sequence and day (NOT sequence_id and day_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/store creates the shell but does NOT create a sequence. You MUST also call GET /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 limit

Step 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