| name | wa-deploy |
| description | Stage 4. Push the generated bot to GitHub, deploy on Render.com, register the public webhook URL with Wasender, and verify a real round-trip message works. |
wa-deploy — Take the bot live
Goal: get the bot onto a public HTTPS URL so Wasender can deliver webhooks to it. Render.com is the default — free tier is enough for a personal bot, and the deploy is a single CLI call.
Speak Hebrew. Ask before doing anything that costs money or makes the bot reachable.
Step 1 — Frame the deploy
הבוט שלך מוכן. עכשיו נעלה אותו לאוויר.
נשתמש ב-Render.com — שירות שמריץ את הבוט בענן. יש להם תוכנית חינמית שמספיקה לרוב המקרים. אם הבוט הופך עמוס נשדרג ל-$7 לחודש.
מוכן?
Step 2 — Push the bot to GitHub
The bot needs to live in a Git repo for Render to deploy it.
Ask:
יש לך חשבון GitHub?
- כן, ואני מחובר ל-
gh CLI כאן
- כן, אבל לא מחובר
- אין לי
For 1: skip ahead.
For 2: tell them to run gh auth login and come back.
For 3: walk them through creating one at github.com/signup, then gh auth login.
Then:
cd bot
git init
git add .
git commit -m "initial commit: WhatsApp agent generated by WhatsApp Agent Builder"
gh repo create wa-bot-${BOT_NAME_SLUG} --private --source . --remote origin --push
Capture the repo URL from gh output. Tell the user:
יצרתי לך ריפו פרטי ב-GitHub: {repo_url}. הקוד שלך נשמר שם.
Step 3 — Sign up at Render
Ask:
יש לך חשבון Render?
- כן
- לא — נירשם עכשיו
For 2:
- גש ל https://render.com/register
- הירשם עם GitHub (הכי קל) או אימייל.
- כשסיימת, תגיד לי "מוכן".
Step 4 — Get a Render API key
בשביל לפרוס מכאן, אצטריך מפתח API של Render:
- גש ל https://dashboard.render.com/u/settings#api-keys
- צור מפתח חדש בשם "claude-code".
- הדבק כאן.
Save to project-root .env as RENDER_API_KEY. Don't echo back.
Step 5 — Create the Render service
Use the Render API (POST /v1/services). The bot is a web_service.
Render API schema notes (verified live, 2026-05):
envVars is top-level in the request body, not inside serviceDetails.
buildCommand and startCommand go inside serviceDetails.envSpecificDetails (NOT top-level, NOT serviceDetails directly). Putting them at the top level returns "buildCommand is required for non-static, non-docker services" even though they're set there.
pythonVersion also goes inside envSpecificDetails.
- Use
runtime: "python" (not the older env: "python" — Render accepts both but runtime is the current key).
Use Python to build the JSON body so secrets stay in env vars and never appear in shell command output:
set -a; source "$ENV_FILE"; set +a
export OWNER_ID="..."
python <<'PYEOF' > /tmp/render_create.json
import json, os
body = {
"type": "web_service",
"name": "wa-bot-" + os.environ["BOT_NAME_SLUG"],
"ownerId": os.environ["OWNER_ID"],
"repo": os.environ["GITHUB_REPO_URL"],
"branch": "main",
"autoDeploy": "yes",
"rootDir": "",
"envVars": [
{"key": "WASENDER_API_KEY", "value": os.environ["WASENDER_API_KEY"]},
{"key": "WASENDER_WEBHOOK_SECRET", "value": os.environ["WASENDER_WEBHOOK_SECRET"]},
{"key": "WASENDER_ACCOUNT_PROTECTION", "value": "true"},
{"key": "ANTHROPIC_API_KEY", "value": os.environ["ANTHROPIC_API_KEY"]},
{"key": "LLM_MODEL", "value": "claude-haiku-4-5"},
{"key": "MAX_HISTORY", "value": "20"},
],
"serviceDetails": {
"runtime": "python",
"plan": "free",
"region": "singapore",
"envSpecificDetails": {
"pythonVersion": "3.12.7",
"buildCommand": "pip install -r requirements.txt",
"startCommand": "uvicorn main:app --host 0.0.0.0 --port $PORT",
},
"healthCheckPath": "/healthz",
},
}
print(json.dumps(body))
PYEOF
curl -s -X POST "https://api.render.com/v1/services" \
-H "Authorization: Bearer $RENDER_API_KEY" \
-H "Content-Type: application/json" \
-d @/tmp/render_create.json
rm -f /tmp/render_create.json
OWNER_ID comes from GET /v1/owners first:
curl -s "https://api.render.com/v1/owners" \
-H "Authorization: Bearer $RENDER_API_KEY" | jq -r '.[0].owner.id'
Region: pick frankfurt for Israeli users (lowest latency to most Israeli ISPs), oregon for Americas, singapore for Asia/Pacific.
Save the returned service.id and service.serviceDetails.url to .wa-state.json as render_service_id and render_url.
Step 6 — Wait for the build
Poll GET /v1/services/{id}/deploys?limit=1 until status becomes live. Show progress in Hebrew:
בונה... (~2 דקות)
מתקין תלויות...
מפעיל...
🎉 הבוט באוויר ב: {render_url}
Verify:
curl -s "$RENDER_URL/healthz"
Expect {"status":"ok"}. If it fails, fetch the latest deploy logs (GET /v1/services/{id}/deploys/{deployId}/events) and surface the error in Hebrew.
Step 7 — Register the webhook URL with Wasender
Now the critical bit: tell Wasender where to deliver inbound messages.
Branch on setup_path from .wa-state.json:
Path A (PAT available) — programmatic
curl -sX PUT "https://www.wasenderapi.com/api/whatsapp-sessions/$WASENDER_SESSION_ID" \
-H "Authorization: Bearer $WASENDER_PAT" \
-H "Content-Type: application/json" \
-d '{
"webhook_url": "'$RENDER_URL'/webhook/wasender",
"webhook_enabled": true,
"webhook_events": ["messages.received", "session.status"]
}'
Verify:
curl -sX GET "https://www.wasenderapi.com/api/whatsapp-sessions/$WASENDER_SESSION_ID" \
-H "Authorization: Bearer $WASENDER_PAT" | jq '.data.webhook_url'
Should return the Render URL.
Path B (no PAT, dashboard-managed session) — manual
The user has to set the webhook URL via the dashboard. Tell them (in Hebrew):
עכשיו רק תעדכן את ה-Webhook ב-Wasender ידנית:
- גש ל-https://wasenderapi.com/dashboard ופתח את הסשן שלך.
- לחץ על Manage Webhook (כפתור ימני עליון).
- הדבק את הכתובת הזו בשדה Webhook URL:
{RENDER_URL}/webhook/wasender
- סמן את האירוע
messages.received (וגם session.status אם רוצים התראה על ניתוקים).
- חשוב: וודא שהשדה Webhook Secret זהה למה שיש ב-
.env שלך (WASENDER_WEBHOOK_SECRET). אם לא, או החלף את אחד מהם, או צור חדש בשני המקומות.
- לחץ Save.
תגיד לי "שמרתי" ואבדוק שהכל עובד.
Wait for confirmation. Then proceed to step 8.
Step 8 — Real round-trip test
Tell the user:
הכל מוכן. שלח עכשיו הודעה אמיתית מהטלפון שלך אל {WASENDER_PHONE}. אני אבדוק שהבוט קיבל וענה.
Wait 30 seconds. Then check Render logs:
curl -s "https://api.render.com/v1/services/$RENDER_SERVICE_ID/logs?limit=20" \
-H "Authorization: Bearer $RENDER_API_KEY"
Look for the inbound webhook line. If found and the user confirms they received a reply on their phone:
🚀 הבוט שלך חי! מספר {WASENDER_PHONE} עונה עכשיו אוטומטית.
If no inbound seen after 60 seconds, run the diagnostic from wa-maintain (don't make the user wait — proactively diagnose).
Step 9 — Update state
Update .wa-state.json:
{
"current_stage": "connect",
"completed_stages": ["setup", "characterize", "build", "deploy"],
"render_service_id": "<id>",
"render_url": "<url>",
"last_updated": "<ISO>"
}
Tell the user:
השלבים הבאים אופציונליים — להוסיף לבוט יכולות נוספות (יומן, מייל, תזכורות מתמשכות) או לבחור איפה ההיסטוריה נשמרת. רוצה להמשיך, או לעצור כאן?
If they want more: route to wa-connect. If stop: route to wa-maintain (idle mode).
Common pitfalls
- Render free tier sleeps after 15 min idle. First message after sleep takes ~30s to wake. Mention this to the user. Suggest the $7 paid tier if always-on matters.
- 404 immediately after a fresh deploy. Even on first deploy, Render's edge can return
404 Not Found (10 bytes, plain text) for ~10–60s while the service is still spinning up internally. The deploy-status API may already say live while curl gets 404s. Don't panic and don't redeploy — wait 30s and retry /healthz. If it's still 404 after 90s, then something is genuinely wrong (check GET /v1/services/{id} for suspended, and check deploy events).
- Build fails on Python version. If Render reports "no matching Python", check
.python-version and/or set PYTHON_VERSION=3.12.7 env var on the service.
- Webhook signature mismatch. Triple-check
WASENDER_WEBHOOK_SECRET env var on Render matches the one Wasender returned at session-create time. If user regenerated the secret in dashboard, update both Render env and bot/.env locally.
- Wasender returns 422 on PUT session. Some fields may be read-only. If updating webhook fails, try
POST /api/whatsapp-sessions/{id}/webhook (some accounts have a dedicated webhook endpoint — verify in their dashboard).
- Account protection rate limits. With
account_protection: true, the bot can only send 1 message per 5 seconds. If a long reply gets split into multiple sends (or there's a burst), they'll queue. Mention this if user complains "the bot sometimes feels slow."