Skip to content
imper.ai

Amazon Connect IVR for Helpdesk

This guide walks you through configuring imper.ai's Amazon Connect IVR module for Helpdesk, including the IAM policy and role, the Lambda function, and the Amazon Connect flow module. It is intended for administrators.

When a caller reaches your Amazon Connect flow, the module invokes a Lambda that calls imper.ai to send the caller a verification SMS and run the Self-Service Portal (SSP) flow. On success the call is disconnected; on failure the call is routed back to an agent.


Prerequisites

  • An AWS account with permissions to create IAM policies and roles, Lambda functions, and Amazon Connect flows.
  • An existing Amazon Connect instance with a queue configured.
  • An imper.ai account with an Admin role.

Setup

Step 1 - Create the IAM policy

Create an IAM policy that allows starting outbound voice contacts from Amazon Connect. This lets imper.ai initiate a callback on your behalf and return the user to the queue. The policy can be found in Appendix 1 - Policy.

The IAM policy for starting outbound voice contacts, in the AWS console.

Step 2 - Create the IAM role (trust policy)

Create a new role with a custom trusted entity, using the trust policy from the admin console. The trust policy can be found in Appendix 2 - Role. Then attach the policy from Step 1 to this role.

This role lets the imper.ai account (external to your account) assume a role that is connected only to the Step 1 policy.

Confirmation of the created IAM role in the AWS console.

When the role is created, paste its role ARN into the imper.ai admin console.

Step 3 - Create the Lambda function (Node.js 22)

  1. Create a basic Lambda function from scratch. The function source can be found in Appendix 3 - Lambda.
Creating a basic Lambda function from scratch in the AWS console.
  1. Download the function ZIP from the imper.ai admin console and upload it when creating the function.
Uploading the function ZIP while creating the Lambda function.
  1. Configure the environment variables. Set IMPER_API_KEY and IMPER_ORG_ID using the values from the imper.ai UI.
Configuring the IMPER_API_KEY and IMPER_ORG_ID environment variables on the Lambda function.
  1. Copy the function's ARN for later use.
  2. In Amazon Connect, under the Flows tab, add the Lambda so it can be invoked.

This Lambda connects from your environment to imper.ai, triggering the verification SMS and the Self-Service Portal flow.

Step 4 - Configure the Amazon Connect module

Perform these steps in the Amazon Connect UI (****.my.connect.aws).

  1. Create a new module and import it from JSON.
Creating a new module and importing it from JSON in Amazon Connect.
  1. Download the module JSON from the imper.ai admin console and import it. The JSON can be found in Appendix 4 - Module.

  2. In your incoming flow, add a Set User Attributes block and set the following user-defined attributes:

    • sms_sending_lambda - the Lambda function ARN from Step 3.
    • callback_flow_id - the FlowId to trigger on callback. This flow should immediately insert the number into the queue and manually validate the user. If no such flow exists, create a simple flow that connects directly to the queue.
The Set User Attributes block configured with sms_sending_lambda and callback_flow_id.
  1. Ensure a queue is set before the module is called (using Set working queue). This block should already run in the flow - just make sure it happens before the module.

  2. Add an Invoke Module block and choose the module you created:

    • If the call should be routed to an agent, the module succeeds and the flow continues to agent routing.
    • If there is any error, the module fails and should be rerouted to an agent.
    • If the SMS is sent successfully, the module disconnects the call and ends the flow.

Troubleshooting

Permission errors

Issue: You receive an "Access Denied" error when performing certain actions.

  • Verify that your IAM policies grant the necessary permissions for Amazon Connect and related services.
  • Ensure that the IAM role associated with your Amazon Connect instance has the appropriate permissions.

Missing queue

Issue: Calls are not routed to the expected queue, or the queue does not appear in the Amazon Connect console.

  • Confirm that the queue is created and active in the Amazon Connect console.
  • Check the contact flow to ensure it references the intended queue.
  • Verify that agents are assigned to the queue and are in a status to receive calls.

Lambda function not listed

Issue: A required Lambda function does not appear in the drop-down when configuring a contact flow.

  • Ensure the Lambda function is in the same AWS region as your Amazon Connect instance.
  • Confirm that the Lambda function's execution role includes the necessary permissions for Amazon Connect.
  • Check that the Lambda function has been granted permission to be invoked by Amazon Connect.

Appendix 1 - Policy

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowStartOutboundVoiceContactUSE1",
      "Effect": "Allow",
      "Action": "connect:StartOutboundVoiceContact",
      "Resource": "arn:aws:connect:*:*:instance/*/contact/*"
    }
  ]
}

Appendix 2 - Role

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowDevUSE1IRSAOnly",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::047719627774:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "aws:PrincipalAccount": "047719627774"
        },
        "StringLike": {
          "aws:PrincipalArn": "arn:aws:iam::047719627774:role/*"
        }
      }
    }
  ]
}

Appendix 3 - Lambda

index.js:

js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.handler = void 0;
// Centralized configuration and constants
const DEFAULT_BASE_URL = 'https://api.prod.imper.ai/external/v1/amazon-connect/';
const DEFAULT_SEND_SMS_URL = 'send-sms';
const DEFAULT_ERROR_URL = 'error';
const DEFAULT_TIMEOUT_MS = 4000;
function getBaseUrl() {
    return process.env.BASE_URL ?? DEFAULT_BASE_URL;
}
function getSendSmsUrl() {
    return process.env.SEND_SMS_URL_PATH ?? DEFAULT_SEND_SMS_URL;
}
function getErrorUrl() {
    return process.env.ERROR_URL_PATH ?? DEFAULT_ERROR_URL;
}
function getAbsoluteUrls() {
    const base = getBaseUrl();
    const baseWithSlash = base.endsWith('/') ? base : `${base}/`;
    const send = `${baseWithSlash}${getSendSmsUrl()}`;
    const error = `${baseWithSlash}${getErrorUrl()}`;
    return { send, error };
}
function getTimeoutMs() {
    return Number(process.env.FETCH_TIMEOUT_MS ?? String(DEFAULT_TIMEOUT_MS));
}
function getApiKey() {
    if (!process.env.IMPER_API_KEY) {
        throw new Error('IMPER_API_KEY is not set');
    }
    return process.env.IMPER_API_KEY;
}
function buildHeaders() {
    const headers = {
        accept: 'application/json',
        'content-type': 'application/json',
    };
    const apiKey = getApiKey();
    if (apiKey)
        headers['x-api-key'] = apiKey;
    return headers;
}
function parseAndValidateParams(event) {
    const params = event?.Details?.Parameters ?? {};
    const chosenNumber = params.callback ?? params.number;
    const queue_id = params.queue_id;
    const instance_id = params.instance_id;
    const callback_flow_id = params.callback_flow_id;
    // Don't hard fail; pass what we have and let the backend defaults apply
    const result = {};
    if (chosenNumber)
        result.number = String(chosenNumber);
    if (queue_id)
        result.queue_id = String(queue_id);
    if (instance_id)
        result.instance_id = String(instance_id);
    if (callback_flow_id)
        result.callback_flow_id = String(callback_flow_id);
    return result;
}
async function callImperSendSms(params, timeoutMs = getTimeoutMs()) {
    const { send } = getAbsoluteUrls();
    const url = new URL(send);
    const controller = new AbortController();
    const id = setTimeout(() => controller.abort(), timeoutMs);
    try {
        const res = await fetch(url.toString(), {
            method: 'POST',
            headers: buildHeaders(),
            body: JSON.stringify({
                number: params.number,
                queue_id: params.queue_id,
                instance_id: params.instance_id,
                callback_flow_id: params.callback_flow_id,
            }),
            signal: controller.signal,
        });
        const contentType = res.headers.get('content-type') ?? '';
        const payload = contentType.includes('application/json') ? await res.json() : await res.text();
        return { ok: res.ok, status: res.status, payload };
    }
    finally {
        clearTimeout(id);
    }
}
async function postError(details, timeoutMs = getTimeoutMs()) {
    const { error } = getAbsoluteUrls();
    const url = new URL(error);
    const controller = new AbortController();
    const id = setTimeout(() => controller.abort(), timeoutMs);
    try {
        await fetch(url, {
            method: 'POST',
            headers: buildHeaders(),
            body: JSON.stringify(details),
            signal: controller.signal,
        });
    }
    catch {
        // Swallow error reporting failures
    }
    finally {
        clearTimeout(id);
    }
}
const handler = async (event, context) => {
    let params;
    const timeoutMs = getTimeoutMs();
    let errorReported = false;
    let lastStatus;
    try {
        params = parseAndValidateParams(event);
        const { ok, status, payload } = await callImperSendSms(params, timeoutMs);
        if (!ok) {
            lastStatus = status;
            await postError({
                reason: 'non_ok_response',
                status,
                response: payload,
                request: params,
                event,
                context: {
                    awsRequestId: context?.awsRequestId,
                    functionName: context?.functionName,
                    invokedFunctionArn: context?.invokedFunctionArn,
                    memoryLimitInMB: context?.memoryLimitInMB,
                },
                env: {
                    SEND_SMS_URL: getBaseUrl(),
                    FETCH_TIMEOUT_MS: String(getTimeoutMs()),
                    IMPER_API_KEY_PRESENT: Boolean(getApiKey()),
                },
            }, timeoutMs);
            errorReported = true;
            throw new Error(`SMS dispatch failed with status ${status}: ${JSON.stringify(payload)}`);
        }
        return {
            success: ok,
            statusCode: status,
            data: payload,
            request: params,
        };
    }
    catch (err) {
        const message = err instanceof Error ? err.message : 'Unknown error';
        const stack = err instanceof Error ? err.stack : undefined;
        if (!errorReported) {
            await postError({
                reason: 'exception',
                error: message,
                stack,
                request: params,
                event,
                context: {
                    awsRequestId: context?.awsRequestId,
                    functionName: context?.functionName,
                    invokedFunctionArn: context?.invokedFunctionArn,
                    memoryLimitInMB: context?.memoryLimitInMB,
                },
                env: {
                    SEND_SMS_URL: getBaseUrl(),
                    FETCH_TIMEOUT_MS: String(getTimeoutMs()),
                },
            }, timeoutMs);
        }
        throw new Error(message);
    }
};
exports.handler = handler;

Appendix 4 - Module

json
{
  "Version": "2019-10-30",
  "StartAction": "b1a72443-4fc7-4022-9223-d92b63696836",
  "Metadata": {
    "entryPointPosition": { "x": 40, "y": 40 },
    "name": "Send Helpdesk SMS prod full",
    "description": "",
    "status": "published"
  },
  "Actions": [
    {
      "Parameters": {},
      "Identifier": "018e36c1-446a-4c32-9fa9-42b9b0bcd823",
      "Type": "DisconnectParticipant",
      "Transitions": {}
    },
    {
      "Parameters": {},
      "Identifier": "a32ddc29-150e-473d-87fb-c915e34df397",
      "Type": "EndFlowModuleExecution",
      "Transitions": {}
    },
    {
      "Parameters": { "Text": "Failed to Send SMS. Rerouting to agent" },
      "Identifier": "9dba262d-69f5-456f-92b3-09285edd16b0",
      "Type": "MessageParticipant",
      "Transitions": {
        "NextAction": "a32ddc29-150e-473d-87fb-c915e34df397",
        "Errors": [ { "NextAction": "a32ddc29-150e-473d-87fb-c915e34df397", "ErrorType": "NoMatchingError" } ]
      }
    },
    {
      "Parameters": { "TimeLimitSeconds": "30" },
      "Identifier": "Wait before Resend",
      "Type": "Wait",
      "Transitions": {
        "NextAction": "a32ddc29-150e-473d-87fb-c915e34df397",
        "Conditions": [ { "NextAction": "Send helpdesk SMS", "Condition": { "Operator": "Equals", "Operands": [ "WaitCompleted" ] } } ],
        "Errors": [ { "NextAction": "a32ddc29-150e-473d-87fb-c915e34df397", "ErrorType": "NoMatchingError" } ]
      }
    },
    {
      "Parameters": {
        "Text": "Succesfully sent SMS.\nPlease confirm receipt by pressing 1.\nTo resend the sms, please press 2.\nTo speak to an agent, please press 9.",
        "StoreInput": "False",
        "InputTimeLimitSeconds": "30"
      },
      "Identifier": "Confirm SMS receipt",
      "Type": "GetParticipantInput",
      "Transitions": {
        "NextAction": "a32ddc29-150e-473d-87fb-c915e34df397",
        "Conditions": [
          { "NextAction": "018e36c1-446a-4c32-9fa9-42b9b0bcd823", "Condition": { "Operator": "Equals", "Operands": [ "1" ] } },
          { "NextAction": "Wait before Resend", "Condition": { "Operator": "Equals", "Operands": [ "2" ] } },
          { "NextAction": "a32ddc29-150e-473d-87fb-c915e34df397", "Condition": { "Operator": "Equals", "Operands": [ "9" ] } }
        ],
        "Errors": [
          { "NextAction": "018e36c1-446a-4c32-9fa9-42b9b0bcd823", "ErrorType": "InputTimeLimitExceeded" },
          { "NextAction": "a32ddc29-150e-473d-87fb-c915e34df397", "ErrorType": "NoMatchingCondition" },
          { "NextAction": "a32ddc29-150e-473d-87fb-c915e34df397", "ErrorType": "NoMatchingError" }
        ]
      }
    },
    {
      "Parameters": {
        "LambdaFunctionARN": "$.Attributes.sms_sending_lambda",
        "InvocationTimeLimitSeconds": "5",
        "InvocationType": "SYNCHRONOUS",
        "LambdaInvocationAttributes": {
          "number": "$.CustomerEndpoint.Address",
          "callback": "$.CallbackNumber",
          "queue_id": "$.Queue.ARN",
          "instance_id": "$.InstanceARN",
          "callback_flow_id": "$.Attributes.callback_flow_id"
        },
        "ResponseValidation": { "ResponseType": "JSON" }
      },
      "Identifier": "Send helpdesk SMS",
      "Type": "InvokeLambdaFunction",
      "Transitions": {
        "NextAction": "Confirm SMS receipt",
        "Errors": [ { "NextAction": "9dba262d-69f5-456f-92b3-09285edd16b0", "ErrorType": "NoMatchingError" } ]
      }
    },
    {
      "Parameters": { "FlowLoggingBehavior": "Disabled" },
      "Identifier": "b1a72443-4fc7-4022-9223-d92b63696836",
      "Type": "UpdateFlowLoggingBehavior",
      "Transitions": { "NextAction": "Send helpdesk SMS" }
    }
  ],
  "Settings": {
    "InputParameters": [],
    "OutputParameters": [],
    "Transitions": [
      { "DisplayName": "Success", "ReferenceName": "Success", "Description": "" },
      { "DisplayName": "Error", "ReferenceName": "Error", "Description": "" }
    ]
  }
}