Skip to main content

Retellai: Callback Routing

Pay close attention to the following

This tutorial is based on using JavaScript based Container Applications. Make sure that you choose the correct runtime for your container application.

In this tutorial, we will learn how to create a Retellai callback routing script using the Cloudonix Container Application Templates (CXML) language. Callback routing is highly useful when working in a contact center scenario, when more control over the participants of an on-going call is required. In this scenario, the inbound call from Reteall and the original inbound call are connected to a conference room.

The Source Code

Inbound call from the world handling

basic assumptions
  • The provided script will handle any inbound call
  • The provided script will insert the inbound caller to a designted conference room
  • The provided script will handle the generate a callback from Reteall to Cloudonix
const httpClient = require('https');

/* Cloudonix API Config */
const cloudonix_token = '{{ Your Cloudonix Domain Application Token}}';

/* ReTell API Config */
const retell_token = '{{ Your Retell API Token }}';
const retell_agent = '{{ Your Retell Agent ID }}';

async function httpsRequest(url, method = 'GET', headers = {}, body = null, requestTimeout = 5000) {

const start = Date.now();

let response;

if (method === 'POST') {
response = await httpClient.post(url, { headers, body });
} else if (method === 'PUT') {
response = await httpClient.put(url, { headers, body });
} else if (method === 'DELETE') {
response = await httpClient.delete(url, { headers });
} else if (method === 'GET') {
response = await httpClient.get(url, { headers });
} else {
throw new Error(`Unsupported method: ${method}`);
}

const elapsed = Date.now() - start;
if (elapsed > requestTimeout) {
console.log(`[httpsRequest] HTTP Request ${method} to ${url} timed out after ${elapsed} mSec`);
return false;
}

const status = response.status || response.statusCode || 0;
const ok = status >= 200 && status < 300;
const responseBody = typeof response.body === 'string'
? response.body
: JSON.stringify(response.body || {});

return {
status,
ok,
body: responseBody,
headers: response.headers || {}
};
}

// Create a new phone call from Reteall to Cloudonix
async function generateReTellCall(ev, conferenceHash) {
const url = 'https://api.retellai.com/v2/create-phone-call';

// SIP headers from Cloudonix
const inboundSipHeaders = JSON.parse(ev.body).SessionData.profile['trunk-sip-headers'];

const retellDynamicVariables = {
cloudonix_callerid: ev.parameters.From,
cloudonix_destination: ev.parameters.To,
cloudonix_token: ev.parameters.Session,
cloudonix_domain: ev.parameters.Domain,
cloudonix_conference_hash: conferenceHash,
cloudonix_conference: conferenceRoom
};

const retellCustomSipHeaders = {
"X-Cloudonix-Conference-Hash": conferenceHash,
"X-Cloudonix-Inbound-Token": cloudonix_token
}

for (const key in inboundSipHeaders) {
retellDynamicVariables[key] = inboundSipHeaders[key];
}

const payload = {
agent_id: retell_agent,
from_number: ev.parameters.From,
to_number: ev.parameters.To,
retell_llm_dynamic_variables: retellDynamicVariables,
custom_sip_headers: retellCustomSipHeaders
};

console.log(`[generateReTellCall] Payload: ${JSON.stringify(payload)}`);

const headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + retell_token
};

try {
const response = await httpsRequest(url, 'POST', headers, JSON.stringify(payload));

if (!response.ok) {
throw new Error(`ReTell call creation failed: ${response.status} - ${response.body}`);
}

const data = JSON.parse(response.body);

console.log(`[generateReTellCall] Success: ${data.call_id}`);
return data.call_id;
} catch (error) {
console.error(`[generateReTellCall] Error:`, error.message || error);
await deleteSession(ev.parameters.Domain, ev.parameters.Session);
return false;
}
}

// Delete a currently active session (hangup the call)
async function deleteSession(domain, session) {
const url = `https://api.cloudonix.io/customers/self/domains/${domain}/sessions/${session}`;

const headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + cloudonix_token
};

console.log(`[deleteSession] Sending DELETE to ${url}`);

const response = await httpsRequest(url, 'DELETE', headers);

if (!response.ok) {
console.error(`[deleteSession] Failed: ${response.status} ${response.body}`);
return false;
}

console.log(`[deleteSession] Success: ${response.status}`);
return true;
}

function buildConferenceCxml(ev, conferenceHash) {
let result = "<Play>https://s3.us-east-2.amazonaws.com/sounds.cloudonix.io/silence/750-milliseconds-of-silence.mp3</Play>";
result += `<Dial callerId="${ev.parameters.From}">\n`;
result += ` <Conference holdMusic="false" endConferenceOnExit="true" beep="false">${conferenceHash}</Conference>`;
result += `</Dial>`;
return result;
}

function response(content) {
let result = `<?xml version="1.0"?>`;
result += `<Response>${content}</Response>`;
return result;
}

exports.handler = async (ev) => {
// Create a unique confernce hash, based on the inbound session ID
let conferenceHash = ev.parameters.Session.slice(-12);
console.log(`[handler] Conference Hash: ${conferenceHash}`);

// Generate an outbound call from Retell to Cloudonix
const retellResult = await generateReTellCall(ev, conferenceHash);

if (!retellResult) {
console.log(`[handler] ReTell call generation failed with result ${retellResult}`);
return response("<Reject reason='rejected' />");
}

// Insert the inbound caller to the designated conference room
const cxmlResponse = buildConferenceCxml(ev, conferenceHash);
return response(cxmlResponse);
};

In-Depth Explanation

  1. The https module is imported to enable HTTP requests functionality.
  2. Configuration variables for the Retell API, including the API token and agent ID, are established.
  3. A custom function named httpsRequest is created to facilitate HTTP requests, incorporating specific timeout settings and mechanisms for handling errors.
  4. The function getRetellCallId is defined to initiate a Retell call using data from a Cloudonix session.
  5. The dialToRetell function is designed to generate an XML response that includes the Dial verb, directing the call towards the specified Retell agent.
  6. A primary function, handler, is outlined to process Cloudonix events, employing the previously defined functions to craft the necessary response.
  7. Within the handler function, getRetellCallId is invoked to obtain a unique ID for the Retell call.
  8. Should the attempt to generate a Retell call ID fail, the process is designed to terminate the call.
  9. In cases of success, an XML response is formulated using the Dial verb to ensure the call is forwarded to the Retell agent.
  10. This XML response is then returned to Cloudonix to complete the call routing process.

Remember to replace the placeholders ({{ Your Retell API Token }}, {{ Your Retell Agent ID }}, {{ Your Cloudonix Domain Application Token }}) with your actual Retell API token, Retell agent ID and Cloudonix Domain Application Key.

Inbound call from Retell handling

basic assumptions
  • The provided script will handle any inbound call that is received from Retell
  • The provided script will insert the inbound call from the AI Agent to the pre-designated conference room
function buildConferenceCxml(ev, conferenceRoom) {
let result = `<Dial callerId="${ev.parameters.From}">\n`;
result += ` <Conference holdMusic="false" endConferenceOnExit="true" beep="false">${conferenceRoom}</Conference>`;
result += `</Dial>`;
return result;
}

function response(content) {
let result = `<?xml version="1.0"?>`;
result += `<Response>${content}</Response>`;
return result;
}

exports.handler = async (ev) => {
// Extract the conference room from the session data
// This assumes that the conference room is included in the session data in the form of "Cloudonix-Conference-Hash"
const conferenceHash = JSON.parse(ev.parameters.SessionData).profile['trunk-sip-headers']['Cloudonix-Conference-Hash'];
console.log(`[handler] Received event conference hash ${conferenceHash}`);

// Build the CXML response to initiate a conference with the specified conference room
const retellRoutingCxml = buildConferenceCxml(ev, conferenceHash);
return response(retellRoutingCxml);
};

In-Depth Explanation

  1. The buildConferenceCxml function is designed to construct an XML response that includes the Dial verb, directing the call towards the specified conference room.
  2. The response function is used to construct an XML response that includes the Response verb, enclosing the provided content.
  3. The handler function is designed to process Cloudonix events, specifically handling inbound calls.
  4. Within the handler function, the conference room is extracted from the session data and used to construct the CXML response.
  5. This XML response is then returned to Cloudonix to complete the call routing process.

Additional Resources