Skip to main content

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

FieldRequiredDescription
domain_nameYesHub domain slug
rowsYesArray of contact rows (max configurable via BULK_SUBMIT_MAX_ROWS, default 10000)
call_typeNoDefault informational
languageNoDefault en
campaign_nameNoStored as batch file_name
source_systemNoe.g. twenty, followupboss
source_org_idNoWorkspace ID or domain slug
source_objectNoDefault person

Per-row fields

FieldRequiredDescription
instructionYesAgent instruction for this contact
phone_numberFor phone/SMSE.164 or North American number
email or email_addressFor channel=emailRecipient email
nameNoDisplay name
channelNophone, sms, email, etc.
call_typeNoRow override
scheduled_timeNoISO or common datetime formats
user_phone_numberNoInitiator/broker phone
email_template_nameNoHub email layout name
email_subjectNoEmail subject
email_template_idNoHub template id
email_template_paramsNoObject of placeholder values
source_record_idNoCRM person id for sync
crm_contact_idNoOptional 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 by ingesting, processing, completed, or failed
  • GET /bulk-calls/batches/{batch_id} — includes ingest_job progress 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 (default data/bulk_ingest)
  • Bulk execution worker pacing (Hub env):
    • BULK_CALL_MAX_CONCURRENT — max parallel /start calls (default 20; 0 = unlimited)
    • BULK_CALL_BATCH_SIZE — rows fetched per poll (default 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 internal POST /start from 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_days applies when creating bulk rows (and new email-pull /start requests). Queued retries that already have a call_logs row (queued_call_sid) are not blocked, so after-hours queues still execute when the call center opens.

Changelog

DateChange
2026-08-12customer_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-16Added GET/PUT /bulk-calls/mapped-last-run for per-account Mapped Upload UI state.
2026-07-05Bulk 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-04Bulk/timeout workers: BULK_CALL_START_TIMEOUT_SECONDS (default 120) for internal /start; timeouts defer/retry instead of marking call logs Failed.
2026-07-04Async 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-04Bulk worker uses bounded parallel /start (BULK_CALL_MAX_CONCURRENT) instead of serial 5/min pacing.