Error codes and an end-to-end example
Error codes
By type
| Status | error.type | Codes on the endpoints of this guide |
|---|---|---|
| 400 | invalid_request_error | caller_number_not_allowed, caller_number_outside_grant_pool, destination_number_invalid, ring_duration_invalid, invalid_filter_value, invalid_cursor |
| 401 | authentication_error | api_key_required, api_key_unknown, api_key_revoked |
| 403 | permission_error | agent_not_in_subgroup, destination_not_allowed, privileged_key_required, root_org_key_required |
| 404 | not_found_error | trunk_not_found, subgroup_not_found, call_not_found, campaign_not_found, import_not_found, agent_not_found (web calls only) |
| 409 | conflict_error | agent_subgroup_ambiguous, agent_not_published, launch_blocked, campaign_transition_invalid, campaign_closed, market_package_not_installed, idempotency_key_in_flight, dnc_decision_without_upload, dnc_screen_required |
| 422 | invalid_request_error | invalid_request (schema) and the campaign codes below |
| 429 | rate_limit_error | api_key_rate_limit_exceeded, dial_queue_full, subgroup_concurrency_limit, subgroup_rate_limit |
| 500 | api_error | internal_error |
| 502 | api_error | dial_failed (synchronous fallback only), contact_file_storage_unavailable |
| 503 | service_unavailable_error | voice_backend_unavailable, agent_dispatch_failed (web calls) |
Campaign error codes
| Status | error.code | Cause |
|---|---|---|
| 404 | campaign_not_found / import_not_found | No such object in your organization; anything outside that boundary is indistinguishable from missing. |
| 409 | launch_blocked | Launch or resume checks did not pass; error.blockers names them. |
| 409 | campaign_transition_invalid | The transition is impossible from the current state. |
| 409 | campaign_closed | calls: the campaign is completed or cancelled. |
| 409 | market_package_not_installed | Contacts cannot be screened for this organization; contact support. |
| 409 | idempotency_key_in_flight | A request with this key is still being processed; wait for its answer. |
| 409 | dnc_decision_without_upload | A dnc decision on a campaign that has no import. |
| 409 | dnc_screen_required | The import has no source-row projection (pre-feature import): on a dnc decision, on ?only=dnc_match, or as a launch blocker. Upload again. |
| 422 | invalid_request | Body fails schema validation; message is an array of field issues. |
| 422 | idempotency_key_required | The Idempotency-Key header is missing or blank. |
| 422 | idempotency_key_reuse | This key was already used for a different request body. |
| 422 | campaign_agent_unknown | No agent with this name in your organization. |
| 422 | campaign_window_invalid | ends_at is not later than starts_at. |
| 422 | campaign_timezone_unknown | Not an IANA zone name. |
| 422 | campaign_classification_invalid | classification is not one of the three documented values. |
| 422 | campaign_spec_invalid | Name length, pace tier, retry configuration, working-hours form or duplicate action rejected. |
| 422 | campaign_open_ends_at_required / campaign_open_ends_at_too_far | A campaign open to call additions has no ends_at, or one beyond the horizon. |
| 422 | dialing_route_unresolved / dialing_route_ambiguous | No binding, or more than one candidate. |
| 422 | dialing_binding_incomplete | route_id or from_number named without trunk_id. |
| 422 | working_hours_wider_than_law | The schedule is wider than the floor (legal window or organization calling window). |
| 422 | attestation_import_id_required | attestation.import_id missing on launch. |
| 422 | dnc_decision_invalid | dnc is neither block nor skip. |
| 422 | contact_rows_rejected | The planned calls could not be stored at all; read the import report. |
| 422 | report_format_invalid | format is neither csv nor xlsx. |
| 422 | report_filter_invalid | only is neither omitted nor dnc_match. |
| 422 | calls_required / calls_batch_too_large | calls is empty, or has more rows than the cap (1000 by default). |
| 422 | attestation_invalid | calls: attestation is not exactly {"confirmed": true}. |
| 422 | calls_rejected | The batch was refused whole because of a data error (or a structural parse refusal); error.import names the rows. |
| 422 | calls_dnc_matched | The batch was refused whole because at least one number is on the do-not-call registry; read the dnc_match report. |
| 502 | contact_file_storage_unavailable | The contact source could not be stored, so the upload was not accepted; retry. |
End-to-end example
BASE_URL="https://app.hubtalk.ai" # the host your dashboard runs on
API_KEY="cfk_…" # the secret shown once when the key was created
# 1. Look up the agent's available lines (optional: omit the binding
# entirely and the platform resolves it itself)
curl -s "$BASE_URL/v1/dialing-routes?agent_id=collections-agent" \
-H "Authorization: Bearer $API_KEY"
# 2. Create the campaign, hand over the numbers and launch — one request
curl -s -X POST "$BASE_URL/v1/campaigns" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: june-collections-2026-09-09" \
-d '{
"name": "june collections",
"agent_id": "collections-agent",
"calls_per_minute": 10,
"timezone": "Asia/Almaty",
"classification": "debt_collection",
"working_hours": [{"days": [0,1,2,3,4], "start": "09:00", "end": "19:00"}],
"attestation": {"confirmed": true},
"planned_calls": [
{"phone": "+77001234567", "name": "Aigerim", "variables": {"debt": "15000"}},
{"phone": "+77007654321", "name": "Daniyar"}
]
}'
# → 201. Check "status": "running" | "scheduled" | "draft".
# "draft" means the reason is named in "blockers": fix it and call
# POST /v1/campaigns/{id}/launch with
# {"attestation": {"confirmed": true, "import_id": "<import.import_id>"}}
# Note: by default retry_config.attempts = 1 — nobody is called back.
# 3. Stream more numbers later (the campaign must not be closed)
curl -s -X POST "$BASE_URL/v1/campaigns/cam-b9b536a86e/calls" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: crm-batch-2026-09-09-17" \
-d '{"calls": [{"phone": "+77009998877", "name": "Symbat"}],
"attestation": {"confirmed": true}}'
# → 201 (accepted whole) or 422 calls_rejected / calls_dnc_matched (refused whole)
# 4. Follow it
curl -s "$BASE_URL/v1/campaigns/cam-b9b536a86e/progress" \
-H "Authorization: Bearer $API_KEY"
# 5. Inspect the problematic rows
curl -s "$BASE_URL/v1/imports/imp-353d158dd2/report?format=csv" \
-H "Authorization: Bearer $API_KEY" -o import-report.csv
# ...and the do-not-call matches on their own
curl -s "$BASE_URL/v1/imports/imp-353d158dd2/report?only=dnc_match&format=csv" \
-H "Authorization: Bearer $API_KEY" -o import-dnc.csv
# 6. Per-call results arrive as the usual call_ended / call_analyzed webhooks —
# a campaign dials the same calls this API places one by one