Hotel Call Automation - Webhook-Driven Call Chaining¶
Conversation: hotel-call-automation-plan Date: 2026-04-02 Client: Grand Estancia (Salem) - first implementation, planned for 20+ hotel clients
Overview¶
Automated call triggering system for hotel operations — check-in welcome calls, wake-up calls, and checkout reminders. Uses AstraPBX (self-hosted Asterisk) with webhook-driven sequential call dispatching. No external scheduling dependency on the VPS — all state and scheduling lives in Firebase/GCP.
Context¶
- SIP Trunk: 10 channels total, Grand Estancia allocated 1 channel (soft limit, monitored not enforced)
- Grand Estancia: 100 rooms, 60-70 bookings/day observed from logs
- AstraPBX: Full control — AMI/ARI, webhook service, click-to-call API
- Existing infra: Firebase Functions, Firestore, Cloud Scheduler, LogsUpdate service with
/grandestancia-salemwebhook
Architecture: Webhook-Driven Call Chaining¶
Why Not Cloud Tasks Concurrency?¶
We own the PBX. Instead of relying on Cloud Tasks max_concurrent_dispatches to serialize calls, we use Asterisk call-end events (via AstraPBX webhooks) to trigger the next call. This is event-driven, zero polling, and gives us real call completion confirmation.
Flow¶
Firestore Queue AstraPBX (VPS) Asterisk
───────────── ────────────────── ─────────────────
[call_1: pending] ──→ POST /click-to-call ──→ PJSIP originate
[call_2: pending] |
[call_3: pending] call ends
[call_4: pending] |
... Webhook fires ←────── ChannelDestroyed/Hangup
|
POST /call-chain-endpoint
|
Pick next pending ──→ POST /click-to-call ──→ ...
from Firestore
Separation of Concerns¶
GCP (stateful, persistent) VPS (stateless, replaceable)
────────────────────────── ──────────────────────────────
Cloud Scheduler (cron triggers) Asterisk + AstraPBX Node.js
Firebase Functions (dispatch logic) ←── Webhooks (call events)
Firestore (queue, state, metrics) ──→ POST /api/v1/calls/click-to-call
- VPS redeployment has zero impact on state — all queue/batch data is in Firestore
- If VPS goes down mid-batch, a stale call checker (scheduled function every 5 min) detects calls stuck in
"calling"status for >3 min and retries or skips
Firestore Data Model¶
Batch Document¶
/hotel_calls/{client_id}/batches/{batch_id}
{
type: "wakeup" | "checkin_welcome" | "checkout_reminder",
scheduled_at: timestamp,
started_at: timestamp | null,
completed_at: timestamp | null,
status: "scheduled" | "in_progress" | "completed",
total: 70,
completed_count: 0,
failed_count: 0,
concurrency_limit: 1 // bump to 2-3 later without code change
}
Queue Document (per guest per call)¶
/hotel_calls/{client_id}/batches/{batch_id}/queue/{room_number}
{
guest_name: "...",
phone: "91XXXXXXXXXX",
room: "204",
status: "pending" | "calling" | "completed" | "failed" | "no_answer",
call_id: null,
attempt: 0,
max_attempts: 2,
started_at: null,
ended_at: null,
duration: null
}
Channel Metrics (for billing)¶
/channel_metrics/{client_id}/daily/{YYYY-MM-DD}
{
total_calls: increment(1),
total_seconds: increment(duration),
peak_concurrent: max(current, active_count_at_call_start),
hourly: {
"07": { calls: increment(1), seconds: increment(duration) },
...
}
}
Concurrency Control Logic¶
On call-end webhook:
1. Mark call completed in Firestore
2. Count currently "calling" docs in this batch
3. If active < concurrency_limit AND pending docs exist:
Pick next pending → fire click-to-call
4. If no pending left AND no active calls:
Mark batch "completed"
concurrency_limit: 1→ strictly sequential- Change to
3later → 3 calls in parallel, each hangup triggers next, always stays at 3 active - This is a Firestore field change, no code change needed
Call Types & Triggers¶
| Call Type | Trigger | When |
|---|---|---|
| Welcome call | Firestore on_document_created on check-in doc | Immediate or +30 min after check-in |
| Wake-up call | Cloud Scheduler (@scheduler_fn.on_schedule) | 7:00 AM daily |
| Checkout reminder | Cloud Scheduler | 9:00 AM daily |
The scheduler/trigger doesn't fire calls directly — it creates a batch doc and fires the first N calls (N = concurrency_limit). The webhook chain handles the rest.
AstraPBX Webhook Setup¶
Register webhook for each hotel org:
POST /api/v1/webhooks
{
org_id: "grandestancia",
url: "https://your-logsupdate-service/astrapbx-call-chain",
events: ["call.completed", "call.failed"],
active: true
}
Webhook payload contains: call_id, duration, disposition, disconnected_by — enough to update queue doc and decide next action.
Failure Handling¶
On call-end webhook:
if disposition == "no_answer" AND attempt < max_attempts:
→ set status: "pending", attempt: +1
→ push to end of queue (retry after all others)
if disposition == "failed" (network/trunk error):
→ pause batch, alert operator
→ could be trunk down, don't burn through queue
Stale Call Checker¶
Scheduled function every 5 min: - Query all calls with status "calling" and started_at > 3 min ago - Mark as failed, trigger next call in queue - Handles: VPS crash, webhook delivery failure, network issues
Firestore Usage Estimate (20 clients, 100 guests/day, 3 call types)¶
| Operation | Daily | Monthly |
|---|---|---|
| Writes | ~32,000 | ~960,000 |
| Reads | ~18,000 | ~535,000 |
| Deletes (cleanup >30d) | ~6,000 | ~182,000 |
| Storage | ~50 MB | ~50 MB |
Cost Breakdown¶
| Free Tier | Usage | Billable | Cost | |
|---|---|---|---|---|
| Writes | 20K/day | 32K/day | 12K/day | ~$0.22/day |
| Reads | 50K/day | 18K/day | 0 | Free |
| Deletes | 20K/day | 6K/day | 0 | Free |
| Storage | 1 GB | 50 MB | 0 | Free |
Monthly Firestore cost: ~$6-7
At 50 clients x 100 guests: ~$33/month. SIP minutes will always be the dominant cost.
Wake-up Call Timing Math¶
- Average call duration: ~30 seconds (ring + IVR message + hangup)
- 70 guests with 1 channel: ~35 minutes to complete all calls
- Window: 7:00 AM - 7:35 AM (acceptable for wake-up)
- With 2 channels: ~17 minutes
- With 3 channels: ~12 minutes
Implementation Steps¶
| Step | What | Where |
|---|---|---|
| 1 | Create call-chain webhook endpoint | LogsUpdate server.py or new Firebase Function |
| 2 | Add batch creation logic for each call type | Firebase Functions main.py |
| 3 | Add Cloud Scheduler triggers for wakeup + checkout | Firebase Functions main.py |
| 4 | Add Firestore trigger for check-in → welcome call batch | Firebase Functions main.py |
| 5 | Register webhooks in AstraPBX for each hotel org | AstraPBX API |
| 6 | Add channel metrics tracking in call-chain endpoint | Same as step 1 |
| 7 | Add stale call checker (every 5 min) | Firebase Functions main.py |
| 8 | Add billing dashboard / report generation | Firebase Functions or separate |
Key Files¶
| File | Role |
|---|---|
FirebaseFunction/functions/main.py | Scheduler triggers, batch creation, Firestore triggers |
LogsUpdate/server.py | Call-chain webhook endpoint, metrics logging |
AstraPBX/src/services/webhookService.js | Fires call-end events to your endpoint |
AstraPBX/src/routes/calls.js | POST /api/v1/calls/click-to-call — originates calls |
AstraPBX/src/services/asterisk/callMonitoringService.js | Real-time channel tracking |