solomonmark.dev · a journal

Serving Dynamic Voice Prompts in Amazon Connect from Amazon S3

Amazon Connect lets you play audio prompts stored in an Amazon S3 bucket directly inside your contact flows — bypassing the need to manually upload every audio file through the admin UI. This guide walks you through the entire setup: bucket creation, permissions, audio file upload using the AWS SDK for JavaScript v3, and wiring it all up in a contact flow.


Why Use S3 for Prompts?

Amazon Connect natively supports prompts uploaded via its admin console, but that approach doesn’t scale well for large libraries. By storing .wav files in S3, you gain:

  • Bulk management — Upload hundreds of prompts without touching the Connect UI
  • Dynamic selection — Choose prompts at runtime based on contact attributes (e.g., language, line of business)
  • Structured storage — Organize files in folders by language, department, or IVR path
  • Programmatic control — Automate uploads, rotations, and updates via the AWS SDK

Prerequisites

Before you start, make sure you have:

  • An active AWS account
  • An existing Amazon Connect instance
  • Node.js (v16+) with npm
  • AWS credentials configured (aws configure or environment variables)
  • Install the required SDK packages:
npm install @aws-sdk/client-s3 @aws-sdk/client-connect

Audio File Requirements

Amazon Connect is strict about audio format. Your .wav files must meet these specifications:

  • Format: .wav only
  • Sample rate: 8 KHz
  • Channels: Mono
  • Encoding: U-Law (µ-Law)
  • Max size: Less than 50 MB
  • Max duration: Less than 5 minutes

Use tools like FFmpeg or Audacity to convert files before uploading:

ffmpeg -i input.mp3 -ar 8000 -ac 1 -acodec pcm_mulaw output.wav

Step 1: Create an S3 Bucket

Use @aws-sdk/client-s3 to create a bucket programmatically.

// createBucket.mjs
import { S3Client, CreateBucketCommand } from "@aws-sdk/client-s3";

const REGION = "us-east-1";
const BUCKET_NAME = "my-connect-prompts-bucket";

const s3Client = new S3Client({ region: REGION });

export async function createPromptsBucket() {
  try {
    const response = await s3Client.send(
      new CreateBucketCommand({ Bucket: BUCKET_NAME }),
    );
    console.log("Bucket created:", response.Location);
  } catch (err) {
    if (err.name === "BucketAlreadyOwnedByYou") {
      console.log("Bucket already exists and is owned by you.");
    } else {
      throw err;
    }
  }
}

createPromptsBucket();

Tip: For regions other than us-east-1, add CreateBucketConfiguration: { LocationConstraint: REGION } to the command params.


Step 2: Apply the S3 Bucket Policy

Amazon Connect needs s3:GetObject and s3:ListBucket permissions on the bucket. You must scope the policy to your specific Connect instance to prevent unauthorized access.

// setBucketPolicy.mjs
import { S3Client, PutBucketPolicyCommand } from "@aws-sdk/client-s3";

const REGION = "us-east-1";
const BUCKET_NAME = "my-connect-prompts-bucket";
const AWS_ACCOUNT_ID = "123456789012";
const CONNECT_INSTANCE_ID = "your-connect-instance-id"; // UUID only

const s3Client = new S3Client({ region: REGION });

const bucketPolicy = {
  Version: "2012-10-17",
  Id: "ConnectS3PromptsPolicy",
  Statement: [
    {
      Sid: "AllowConnectToReadPrompts",
      Effect: "Allow",
      Principal: {
        Service: "connect.amazonaws.com",
      },
      Action: ["s3:GetObject", "s3:ListBucket"],
      Resource: [
        `arn:aws:s3:::${BUCKET_NAME}`,
        `arn:aws:s3:::${BUCKET_NAME}/*`,
      ],
      Condition: {
        StringEquals: {
          "aws:SourceAccount": AWS_ACCOUNT_ID,
          "aws:SourceArn": `arn:aws:connect:${REGION}:${AWS_ACCOUNT_ID}:instance/${CONNECT_INSTANCE_ID}`,
        },
      },
    },
  ],
};

async function applyBucketPolicy() {
  const command = new PutBucketPolicyCommand({
    Bucket: BUCKET_NAME,
    Policy: JSON.stringify(bucketPolicy),
  });

  await s3Client.send(command);
  console.log("Bucket policy applied successfully.");
}

applyBucketPolicy();

Security note: Always include the Condition block. Without it, any Amazon Connect instance in your account can access the bucket.


Step 3: Upload Audio Prompts to S3

Organize prompts in folders (e.g., by language or department).

Single File Upload

// uploadPrompt.mjs
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { readFileSync } from "fs";

const REGION = "us-east-1";
const BUCKET_NAME = "my-connect-prompts-bucket";

const s3Client = new S3Client({ region: REGION });

export async function uploadPrompt(localFilePath, s3Key) {
  const fileContent = readFileSync(localFilePath);

  const command = new PutObjectCommand({
    Bucket: BUCKET_NAME,
    Key: s3Key, // e.g., "en/welcome-greeting.wav"
    Body: fileContent,
    ContentType: "audio/wav",
  });

  const response = await s3Client.send(command);
  console.log(`Uploaded ${s3Key} — ETag: ${response.ETag}`);
}

// Example usage
await uploadPrompt("./audio/welcome-greeting.wav", "en/welcome-greeting.wav");
await uploadPrompt("./audio/main-menu.wav", "en/main-menu.wav");
await uploadPrompt("./audio/bienvenida.wav", "es/welcome-greeting.wav");

Batch Upload (Entire Folder)

// batchUploadPrompts.mjs
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { readdirSync, readFileSync, statSync } from "fs";
import { join, relative } from "path";

const s3Client = new S3Client({ region: "us-east-1" });
const BUCKET_NAME = "my-connect-prompts-bucket";

async function uploadDirectory(localDir, s3Prefix = "") {
  const files = [];

  function collectFiles(dir) {
    for (const entry of readdirSync(dir)) {
      const fullPath = join(dir, entry);
      statSync(fullPath).isDirectory()
        ? collectFiles(fullPath)
        : files.push(fullPath);
    }
  }

  collectFiles(localDir);

  await Promise.all(
    files.map(async (filePath) => {
      const relativePath = relative(localDir, filePath).replace(/\\/g, "/");
      const s3Key = s3Prefix ? `${s3Prefix}/${relativePath}` : relativePath;

      await s3Client.send(
        new PutObjectCommand({
          Bucket: BUCKET_NAME,
          Key: s3Key,
          Body: readFileSync(filePath),
          ContentType: "audio/wav",
        }),
      );
      console.log(`Uploaded: ${s3Key}`);
    }),
  );
}

// Upload ./prompts → s3://my-connect-prompts-bucket/
await uploadDirectory("./prompts", "");

Step 4: Reference Prompts in a Contact Flow

Amazon Connect’s Play Prompt block supports three reference styles.

MethodFormatUse Case
HTTPS URLhttps://{bucket}.s3.amazonaws.com/{folder}/{file}.wavStatic, hardcoded prompts
S3 URIs3://{bucket}/{folder}/{file}.wavCleaner syntax, same behavior
Contact Attribute$.Attributes.promptUrlDynamic, runtime selection

Programmatically Update a Contact Flow via SDK

// updateContactFlow.mjs
import {
  ConnectClient,
  UpdateContactFlowContentCommand,
} from "@aws-sdk/client-connect";

const REGION = "us-east-1";
const INSTANCE_ID = "your-connect-instance-id";
const CONTACT_FLOW_ID = "your-contact-flow-id";
const BUCKET_NAME = "my-connect-prompts-bucket";

const connectClient = new ConnectClient({ region: REGION });

const contactFlowContent = JSON.stringify({
  Version: "2019-10-30",
  StartAction: "play-prompt-action",
  Actions: [
    {
      Identifier: "play-prompt-action",
      Type: "MessageParticipant",
      Parameters: {
        Text: "",
        PromptId: "",
        Media: {
          Uri: `https://${BUCKET_NAME}.s3.amazonaws.com/en/welcome-greeting.wav`,
          SourceType: "S3",
          MediaType: "Audio",
        },
      },
      Transitions: {
        NextAction: "end-flow",
        Errors: [{ NextAction: "end-flow", ErrorType: "Any" }],
        Conditions: [],
      },
    },
    {
      Identifier: "end-flow",
      Type: "DisconnectParticipant",
      Parameters: {},
      Transitions: {},
    },
  ],
});

async function updateFlow() {
  const command = new UpdateContactFlowContentCommand({
    InstanceId: INSTANCE_ID,
    ContactFlowId: CONTACT_FLOW_ID,
    Content: contactFlowContent,
  });

  await connectClient.send(command);
  console.log("Contact flow updated successfully.");
}

updateFlow();

Step 5: Dynamic Prompt Selection with Contact Attributes

One of the most powerful use cases is choosing a prompt at runtime — for example, selecting the language-specific greeting based on the caller’s profile.

In the contact flow editor:

  1. Add a Set contact attributes block before the Play prompt block
  2. Set a user-defined attribute, e.g. Key: promptUrl, Value: https://my-connect-prompts-bucket.s3.amazonaws.com/es/welcome-greeting.wav
  3. In the Play prompt block, set the source to Attribute and reference $.Attributes.promptUrl

You can also set this attribute dynamically from a Lambda function that looks up the caller’s preferred language from a database, making the entire IVR experience localized and personalized.


Step 6: Error Handling

If Amazon Connect cannot find or download the file, the Play Prompt block follows the Error branch. Always connect the Error branch in your contact flow to a fallback prompt or a disconnect block. A CloudWatch log entry for a failed prompt looks like:

{
  "Results": "Unable To Download Prompt From S3.",
  "ContactFlowModuleType": "PlayPrompt",
  "Parameters": {
    "PromptLocation": "s3://my-connect-prompts-bucket/en/missing-file.wav",
    "PromptSource": "S3"
  }
}

Common causes include:

  • File not found at the specified S3 key (typo in path)
  • Bucket policy missing or incorrect
  • AWS managed KMS key encryption on the bucket (use a customer-managed key instead)
  • File not in the correct .wav U-Law 8KHz format
  • File exceeding 50 MB or 5 minutes in duration

Cleanup

To avoid ongoing S3 storage costs, delete the bucket and its contents when done testing:

// cleanup.mjs
import {
  S3Client,
  DeleteObjectCommand,
  DeleteBucketCommand,
} from "@aws-sdk/client-s3";

const s3Client = new S3Client({ region: "us-east-1" });
const BUCKET_NAME = "my-connect-prompts-bucket";

await s3Client.send(
  new DeleteObjectCommand({
    Bucket: BUCKET_NAME,
    Key: "en/welcome-greeting.wav",
  }),
);
await s3Client.send(new DeleteBucketCommand({ Bucket: BUCKET_NAME }));
console.log("Bucket deleted.");

Architecture Summary

Caller dials number


Amazon Connect Contact Flow


[Set Contact Attribute]   ← (optional: Lambda sets language/prompt path)


[Play Prompt Block]
  PromptSource: S3
  PromptLocation: s3://my-bucket/en/welcome.wav

   ┌───┴───┐
Success   Error
   │         │
   ▼         ▼
Next     Fallback / Disconnect
Block

This architecture gives you a scalable, maintainable IVR prompt system where your audio assets live independently in S3 and can be updated, versioned, or swapped without ever touching the Amazon Connect admin UI.