Bulk Calls JSON API
Engagement Hub supports bulk engagements via file upload (POST /bulk-calls/upload) and JSON submit (POST /bulk-calls/submit). CRM integrations and the Follow Up Boss campaign embed use the JSON endpoint.
Authentication
Same as file upload: API key with call_management permission.
Authorization: Bearer {API_KEY}
POST /bulk-calls/submit
Campaign-level body
| Field | Required | Description |
|---|---|---|
domain_name | Yes | Hub domain slug |
rows | Yes | Array of contact rows (max configurable via BULK_SUBMIT_MAX_ROWS, default 10000) |
call_type | No | Default informational |
language | No | Default en |
campaign_name | No | Stored as batch file_name |
source_system | No | e.g. twenty, followupboss |
source_org_id | No | Workspace ID or domain slug |
source_object | No | Default person |
Per-row fields
| Field | Required | Description |
|---|---|---|
instruction | Yes | Agent instruction for this contact |
phone_number | For phone/SMS | E.164 or North American number |
email or email_address | For channel=email | Recipient email |
name | No | Display name |
channel | No | phone, sms, email, etc. |
call_type | No | Row override |
scheduled_time | No | ISO or common datetime formats |
user_phone_number | No | Initiator/broker phone |
email_template_name | No | Hub email layout name |
email_subject | No | Email subject |
email_template_id | No | Hub template id |
email_template_params | No | Object of placeholder values |
source_record_id | No | CRM person id for sync |
crm_contact_id | No | Optional contact hint |
Example (Aventora CRM)
{
"domain_name": "aventora",
"campaign_name": "June SMS follow-up",
"source_system": "twenty",
"source_org_id": "workspace-uuid",
"call_type": "informational",
"rows": [
{
"phone_number": "+14165550000",
"instruction": "Confirm appointment time",
"name": "Jane Doe",
"channel": "sms",
"source_record_id": "person-uuid"
}
]
}
Response (202 Accepted)
Upload and JSON submit return 202 immediately after validation. Rows are queued in a background ingest worker; poll batch status for progress.
{
"success": true,
"accepted": true,
"status": "ingesting",
"batch_id": "uuid",
"job_id": "uuid",
"total_rows": 2500,
"file_name": "campaign.csv",
"queued_calls": 0,
"skipped_calls": 0,
"errors": [],
"message": "Bulk ingest accepted (2500 rows). Poll GET /bulk-calls/batches/{batch_id} for progress."
}
When ingest completes, batch status moves to processing and queued_calls reflects rows written to the outbound queue. The existing bulk worker then starts /start for each queued call.
Batch detail ingest progress
GET /bulk-calls/batches/{batch_id} includes an ingest_job object while ingest is pending or for historical reference:
{
"batch": { "status": "ingesting", "total_calls": 2500, "queued_calls": 0 },
"ingest_job": {
"status": "processing",
"rows_total": 2500,
"rows_processed": 1200,
"queued_calls": 1180,
"skipped_calls": 20
},
"calls": [],
"call_count": 0
}
Batch status
GET /bulk-calls/batches— filter byingesting,processing,completed, orfailedGET /bulk-calls/batches/{batch_id}— includesingest_jobprogress when applicable
Mapped Upload last-run (admin UI)
Engagement Hub Mapped Upload stores the last successful form defaults, column mapping, and segment progress per account (not in the browser):
GET /bulk-calls/mapped-last-run?account_id=…→{ "last_run": { … } | null }PUT /bulk-calls/mapped-last-run?account_id=…— body is the last-run snapshot (fileName,headers,rowCount, defaults,columnMapping,segmentSize,segmentIndex, …)
Admin UI proxies these via /api/phone/bulk-calls/mapped-last-run. Integrators rarely need this; it is for Hub admin continuity across devices and multi-account browsers.
Limits
- Max rows per JSON submit:
BULK_SUBMIT_MAX_ROWS(default 10000; protects request size) - CSV upload: no hard row cap; ingest runs in background chunks
- Async ingest worker (Hub env):
BULK_INGEST_CHUNK_SIZE— rows processed per chunk (default 500)BULK_INGEST_WORKER_INTERVAL_SECONDS— poll interval (default 5)BULK_INGEST_STORAGE_DIR— temp pickle storage (defaultdata/bulk_ingest)
- Bulk execution worker pacing (Hub env):
BULK_CALL_MAX_CONCURRENT— max parallel/startcalls (default 20;0= unlimited)BULK_CALL_BATCH_SIZE— rows fetched per poll (default 2× max concurrent, or 50 when unlimited)BULK_CALL_WORKER_INTERVAL_SECONDS— poll interval (default 30)BULK_CALL_RATE_LIMIT— legacy min delay between starts in calls/min (default 0 = disabled)BULK_CALL_START_TIMEOUT_SECONDS— HTTP timeout for internalPOST /startfrom bulk/timeout workers (default 120; min 30). Read timeouts leave rows queued for retry instead of marking Failed.
- Per-account Twilio limits (
max_sms_per_number_per_10min, etc.) still apply inside/start customer_dedup_daysapplies when creating bulk rows (and new email-pull/startrequests). Queued retries that already have acall_logsrow (queued_call_sid) are not blocked, so after-hours queues still execute when the call center opens.
Changelog
| Date | Change |
|---|---|
| 2026-08-12 | customer_dedup_days applies only to new bulk/email-pull conversations. Overnight after-hours queues are not marked Incomplete on the next-morning /start retry. |
| 2026-07-16 | Added GET/PUT /bulk-calls/mapped-last-run for per-account Mapped Upload UI state. |
| 2026-07-05 | Bulk ingest no longer books Google/Outlook calendar events for conversational or informational rows when scheduled_time is adjusted; deferrals update scheduled_time in the database only. |
| 2026-07-04 | Bulk/timeout workers: BULK_CALL_START_TIMEOUT_SECONDS (default 120) for internal /start; timeouts defer/retry instead of marking call logs Failed. |
| 2026-07-04 | Async bulk ingest: upload and JSON submit return 202; rows queued in background (bulk_ingest_jobs). Batch status ingesting until rows are written, then processing. |
| 2026-07-04 | Bulk worker uses bounded parallel /start (BULK_CALL_MAX_CONCURRENT) instead of serial 5/min pacing. |