Task:ALO-voice-detail-dtmf | Platform:CPaaS 2.0
General Authentication:Header
Authorization(API key) is required. HeaderX-Tenant-IDoptional — if present, Kong will inject; if not present, the backend will resolve tenantId throughVsaDAO.getTenantByApiKey(). Do not use JWT (x-access-token).
Base URL:Dev:
https://xapi-dev.alohub.vn| Prod:https://xapi.alohub.vn
/v1/voice/detail-dtmfRetrieve 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=13ANDCONNECTED_TIME IS NOT NULL= success. Any value other than 0 and not 13 = filter failed calls.contactStatus=0or not provided = retrieve all.
{
"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 |
|---|---|---|---|
| string (ISO 8601) | Yes | Start time of the filter range. Format: |
| string (ISO 8601) | Yes | End time of the filter range. Note typo:field name is |
| number (integer) | No | Filter by campaign ID. Omit = retrieve all campaigns of the tenant. |
| string | No | Filter by phone number — partial match (LIKE |
| string | No | Filter by DTMF key pressed by the customer — partial match (LIKE |
| number (long) | No |
|
| string | No | Filter by |
| number | No | Page number, starting from |
| number | No | Number of records per page. Default: |
Gotcha:Field backend is
callEndtime(typo — lowercaset, notcallEndTime). FE must send the correct name, if incorrect BE will not apply filter endTime and return all data.
# 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)
{
"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"
}
]
}
Field | Type | Description |
|---|---|---|
| string | "1" = success, "0" = error |
| string |
|
| number | Total number of records matching the filter (used for pagination calculation) |
| string | Customer's phone number |
| string | Start time of the call — format |
| string | Time when agent/system connected — format |
| number | Call duration (seconds) — calculated from |
| string |
|
| string | DTMF key pressed by the customer during the call. Empty if no key pressed. |
| string | Call identifier — format |
| string | Campaign code (set by the creator, may be empty). |
| string | Campaign name. |
| string | Campaign type — E.g.: |
| number | SIP response code of the call (200=answered, 486=busy, 408=timeout, ...). |
| string | Name of the recording file (format: |
Note on time:Fields
timeStartandconnectTimeare string formatdd/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 byCALL_ID DESC(most recent calls first).
Input value | BE Behavior | Returned data |
|---|---|---|
| Do not apply contactStatus filter | All calls |
| 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 |
HTTP | error_code | Description | FE handling |
|---|---|---|---|
401 | UNAUTHORIZED | Missing header | Redirect to re-enter key |
403 | INSUFFICIENT_SCOPE | Key lacks scope | Notify admin |
400 | INVALID_INPUT |
| Show error, request to select a time range |
429 | RATE_LIMIT_EXCEEDED | Exceeded request limit | Retry after |
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 checkerror_codeto differentiate:INVALID_INPUT= user input error;FAIL= system error.
Header | Description |
|---|---|
| Tenant limit/10s |
| Remaining tenant/10s |
| Route limit/10s |
| Remaining route/10s |
| Seconds to wait when hit 429 |