Voice APIDetailed Autocall Report

Detailed Autocall Report

Nhữ Hào Nam·7/8/2026

API Voice — DTMF Details

Task:ALO-voice-detail-dtmf  |  Platform:CPaaS 2.0

General Authentication:Header Authorization(API key) is required. Header X-Tenant-IDoptional — if present, Kong will inject; if not present, the backend will resolve tenantId through VsaDAO.getTenantByApiKey(). Do not use JWT (x-access-token).

Base URL:Dev: https://xapi-dev.alohub.vn |  Prod: https://xapi.alohub.vn


POST /v1/voice/detail-dtmf

Retrieve the detailed list of outbound call results for the auto-call campaign, along with the DTMF information the customer pressed. Supports multi-dimensional filtering (campaignId, phone number, dtmf, status, transactionId) and server-side pagination.

Authentication:Header Authorization(API key), scope: voice

Note method:Endpoint uses POST(not GET) because the filters are passed through the request body. Date range (callStartTime+ callEndtime) is required— missing will result in 400.

contactStatus logic: contactStatus=13AND CONNECTED_TIME IS NOT NULL= success. Any value other than 0 and not 13 = filter failed calls. contactStatus=0or not provided = retrieve all.

Request Body

{
  "callStartTime": "2026-04-01T00:00:00.000Z",
  "callEndtime":   "2026-04-30T23:59:59.000Z",
  "campaignId":    1042,
  "phoneNumber":   "0901234567",
  "dtmf":          "1",
  "contactStatus": 13,
  "transactionId": "TXN-20260401-001",
  "page":          1,
  "limit":         20
}

Field

Type

Required

Description

callStartTime

string (ISO 8601)

Yes

Start time of the filter range. Format: yyyy-MM-ddTHH:mm:ss.SSSZ. Missing → 400.

callEndtime

string (ISO 8601)

Yes

End time of the filter range. Note typo:field name is callEndtime(lowercase t). Missing → 400.

campaignId

number (integer)

No

Filter by campaign ID. Omit = retrieve all campaigns of the tenant.

phoneNumber

string

No

Filter by phone number — partial match (LIKE %value%, case-insensitive).

dtmf

string

No

Filter by DTMF key pressed by the customer — partial match (LIKE %value%, case-insensitive). E.g.: "1"to filter calls where the customer pressed key 1.

contactStatus

number (long)

No

13= only retrieve successful calls (CONTACT_STATUS=13 AND CONNECTED_TIME IS NOT NULL). Any value ≠ 0 and ≠ 13 = filter failed calls. 0or not provided = all.

transactionId

string

No

Filter by EXTERNAL_IDof the campaign customer — used to query by external transaction code.

page

number

No

Page number, starting from 1. Default: 1.

limit

number

No

Number of records per page. Default: 20. Recommended max 100.

Gotcha:Field backend is callEndtime(typo — lowercase t, not callEndTime). FE must send the correct name, if incorrect BE will not apply filter endTime and return all data.

Sample Code

# Không filter — toàn bộ trong khoảng thời gian
curl -X POST "https://xapi.alohub.vn/v1/voice/detail-dtmf" \
  -H "Authorization: sk_live_xxx" \
  -H "X-Tenant-ID: 1527" \
  -H "Content-Type: application/json" \
  -d '{
    "callStartTime": "2026-04-01T00:00:00.000Z",
    "callEndtime":   "2026-04-30T23:59:59.000Z",
    "page": 1,
    "limit": 20
  }'

# Filter theo campaignId + cuộc gọi thành công + phím DTMF=1
curl -X POST "https://xapi.alohub.vn/v1/voice/detail-dtmf" \
  -H "Authorization: sk_live_xxx" \
  -H "X-Tenant-ID: 1527" \
  -H "Content-Type: application/json" \
  -d '{
    "callStartTime": "2026-04-01T00:00:00.000Z",
    "callEndtime":   "2026-04-30T23:59:59.000Z",
    "campaignId": 1042,
    "contactStatus": 13,
    "dtmf": "1",
    "page": 1,
    "limit": 20
  }'

# Filter theo transactionId
curl -X POST "https://xapi.alohub.vn/v1/voice/detail-dtmf" \
  -H "Authorization: sk_live_xxx" \
  -H "X-Tenant-ID: 1527" \
  -H "Content-Type: application/json" \
  -d '{
    "callStartTime": "2026-04-01T00:00:00.000Z",
    "callEndtime":   "2026-04-30T23:59:59.000Z",
    "transactionId": "TXN-20260401-001",
    "page": 1, "limit": 20
  }'
const axios = require('axios');

// Helper: build filter payload
function buildFilter(opts = {}) {
  return {
    callStartTime: opts.callStartTime,  // ISO 8601
    callEndtime:   opts.callEndtime,    // ⚠️ typo: lowercase t
    campaignId:    opts.campaignId,
    phoneNumber:   opts.phoneNumber,
    dtmf:          opts.dtmf,
    contactStatus: opts.contactStatus,  // 13=success, !=0&&!=13=fail, omit=all
    transactionId: opts.transactionId,
    page:          opts.page  ?? 1,
    limit:         opts.limit ?? 20,
  };
}

const response = await axios.post(
  '{{host}}/api/v1/voice/detail-dtmf',
  buildFilter({
    callStartTime: '2026-04-01T00:00:00.000Z',
    callEndtime:   '2026-04-30T23:59:59.000Z',
    campaignId: 1042,
    contactStatus: 13,
    dtmf: '1',
    page: 1, limit: 20,
  }),
  { headers: { 'Authorization': '{{api-key}}', 'X-Tenant-ID': '{{tenant-id}}' } }
);
console.log(response.data);

// Tính tổng trang
const { totalRecord, data } = response.data;
const totalPages = Math.ceil(totalRecord / 20);
import requests

def get_voice_detail_dtmf(
    call_start: str,
    call_end: str,
    campaign_id: int = None,
    phone_number: str = None,
    dtmf: str = None,
    contact_status: int = None,
    transaction_id: str = None,
    page: int = 1,
    limit: int = 20,
):
    payload = {
        "callStartTime": call_start,
        "callEndtime":   call_end,   # ⚠️ typo: lowercase t
        "page": page,
        "limit": limit,
    }
    if campaign_id:     payload["campaignId"]    = campaign_id
    if phone_number:    payload["phoneNumber"]   = phone_number
    if dtmf:            payload["dtmf"]          = dtmf
    if contact_status:  payload["contactStatus"] = contact_status
    if transaction_id:  payload["transactionId"] = transaction_id

    resp = requests.post(
        "{{host}}/api/v1/voice/detail-dtmf",
        json=payload,
        headers={
            "Authorization": "{{api-key}}",
            "X-Tenant-ID":   "{{tenant-id}}",
        }
    )
    return resp.json()

# Chỉ cuộc gọi thành công, bấm phím 1
result = get_voice_detail_dtmf(
    "2026-04-01T00:00:00.000Z",
    "2026-04-30T23:59:59.000Z",
    campaign_id=1042,
    dtmf="1",
    contact_status=13,
)
print(result)

Response 200

{
  "success": "1",
  "error_code": "SUCCESS",
  "error_message": "SUCCESS",
  "totalRecord": 150,
  "data": [
    {
      "phoneNumber":   "0901234567",
      "timeStart":     "01/04/2026 09:30:00",
      "connectTime":   "01/04/2026 09:30:08",
      "duration":      45,
      "contactStatus": "Thanh cong",
      "dtmf":          "1",
      "callId":        "20260401093000-ABCDEFGH-001",
      "campaignCode":  "CP-001",
      "campaignName":  "Chiến dịch tháng 4",
      "campaignType":  "CAMPAIGN_CALL_AUTO",
      "sipCode":       200,
      "url":           "20260401093000-ABCDEFGH-001.mp3"
    }
  ]
}

Response Fields

Field

Type

Description

success

string

"1" = success, "0" = error

error_code

string

SUCCESSon success

totalRecord

number

Total number of records matching the filter (used for pagination calculation)

data[].phoneNumber

string

Customer's phone number

data[].timeStart

string

Start time of the call — format dd/MM/yyyy HH:mm:ss(UTC+7, not ISO 8601)

data[].connectTime

string

Time when agent/system connected — format dd/MM/yyyy HH:mm:ss. Empty if not connected.

data[].duration

number

Call duration (seconds) — calculated from CONNECTED_TIMEto END_TIME. 0if not connected.

data[].contactStatus

string

"Thanh cong"if CONTACT_STATUS=13 and has CONNECTED_TIME. "Khong thanh cong"in other cases.

data[].dtmf

string

DTMF key pressed by the customer during the call. Empty if no key pressed.

data[].callId

string

Call identifier — format YYYYMMDDHHmmss-XXXXXXXX-NNN. Used to query details.

data[].campaignCode

string

Campaign code (set by the creator, may be empty).

data[].campaignName

string

Campaign name.

data[].campaignType

string

Campaign type — E.g.: CAMPAIGN_CALL_AUTO. Only campaign types in the list_campaign_type_record_ableconfiguration have recording files.

data[].sipCode

number

SIP response code of the call (200=answered, 486=busy, 408=timeout, ...).

data[].url

string

Name of the recording file (format: {callId}.mp3). Empty if the call was unsuccessful or the campaign type does not support recording.

Note on time:Fields timeStartand connectTimeare string format dd/MM/yyyy HH:mm:ssaccording to UTC+7 — not ISO 8601. FE needs to manually parse.

Note on recording URL:Field urlonly contains file name(not full URL). FE needs to concatenate with the base URL of the CDN/storage to play audio. Empty if: call failed, or campaignType is not in the recording whitelist.

Pagination:Offset formula: page=1 → offset=0; page=N → offset = limit*(N-1). Total pages = Math.ceil(totalRecord / limit). Results sorted by CALL_ID DESC(most recent calls first).

contactStatus — Logic filter

Input value

BE Behavior

Returned data

0or not provided

Do not apply contactStatus filter

All calls

13

CONTACT_STATUS=13 AND CONNECTED_TIME IS NOT NULL

"Successful" — connected and completed

Any other value (e.g.: 1, 2, ...)

CONTACT_STATUS≠13 OR (CONTACT_STATUS=13 AND CONNECTED_TIME IS NULL)

"Unsuccessful" — could not connect

Error Codes

HTTP

error_code

Description

FE handling

401

UNAUTHORIZED

Missing header Authorizationor unable to resolve tenantId

Redirect to re-enter key

403

INSUFFICIENT_SCOPE

Key lacks scope voice

Notify admin

400

INVALID_INPUT

callStartTimeor callEndtimeis missing

Show error, request to select a time range

429

RATE_LIMIT_EXCEEDED

Exceeded request limit

Retry after Retry-Afterseconds

400

FAIL

Other processing errors (parse date, DB error, ...)

General error toast, log error_message for debugging

500

FAIL

System error

General error toast

Note error 400 vs 500:BE returns 400for both INVALID_INPUT errors and undefined exceptions (catch-all in controller). FE should check error_codeto differentiate: INVALID_INPUT= user input error; FAIL= system error.

Rate Limit Headers

Header

Description

X-RateLimit-Limit-Tenant

Tenant limit/10s

X-RateLimit-Remaining-Tenant

Remaining tenant/10s

X-RateLimit-Limit-Route

Route limit/10s

X-RateLimit-Remaining-Route

Remaining route/10s

Retry-After

Seconds to wait when hit 429


Was this article helpful?
Updated: 7/8/2026
để chuyển bài