Skip to content

How do I capture a credit card number in Amazon Connect without storing or exposing it to the agent?

13 minute read
Content level: Advanced
2

For sensitive caller inputs (PAN, CVV, authentication codes), post-call redaction is not enough PCI DSS v4.0 requires CVV is never stored after authorization and PAN only stored encrypted. This article describes two capture patterns that keep sensitive data out of the recording and away from the agent: Connect's Store customer input block with public-key DTMF encryption and Amazon Lex + AWS Lambda + AWS KMS for spoken input. It also covers pausing and resuming recording around the secure segment

Short description

Two approaches reduce PCI scope by ensuring sensitive data never lands in a recording or in front of an agent:

  • DTMF encryption with Store customer input — the contact flow prompts the caller to enter digits on the keypad, encrypts them with a public-key certificate uploaded to your Connect instance, and emits ciphertext only. This is the Connect-native pattern most commonly used to take Connect contact centers out of cardholder-data-environment (CDE) scope for digit capture.
  • Lex + Lambda + AWS KMS — for voice or chat input that a Lex bot collects, a Lambda fulfillment function encrypts the slot value using the AWS Encryption SDK with a customer-managed CMK and returns ciphertext.

In both patterns, you suspend call recording around the secure segment using either a flow block or the SuspendContactRecording / ResumeContactRecording API. The PAN never appears in the recording, the Contact Lens transcript, the agent's screen, or contact-flow logs.

Resolution

1. Pause recording around the secure segment

Whichever capture pattern you use, the audio of the caller speaking the digits — or the DTMF tones themselves — must not be recorded. DTMF tones are part of the audio stream and Connect does not automatically strip them, so suspending recording is required, not optional.

Option A: Flow block (declarative)

Place a Set recording, analytics and processing behavior block before the capture, set recording behavior so neither agent nor customer audio is recorded, perform the capture, then place another block after to resume recording for both participants. This is the simplest approach and works for both DTMF and Lex flows. (The older Set recording and analytics behavior block also works but is now legacy.)

Option B: API (programmatic)

For agent-controlled or screen-pop-driven secure entry, use the Amazon Connect API. ContactRecordingType is optional — when omitted, every recording type configured for the contact is suspended, which is the safest default. When you do specify it, valid values are AGENT | IVR | SCREEN. Use IVR while the caller is in self-service (Pattern A), AGENT after the contact reaches an agent (Pattern B with agent-driven secure entry), and SCREEN for screen recording.

import boto3

connect = boto3.client("connect")

# Suspend all configured recording types during a self-service capture.
# Omit ContactRecordingType to suspend everything; specify it to scope to a
# single recording type (AGENT | IVR | SCREEN).
connect.suspend_contact_recording(
    InstanceId=instance_id,
    ContactId=contact_id,
    InitialContactId=initial_contact_id,
    # ContactRecordingType="IVR",   # uncomment to scope to IVR only
)

# ... capture happens here ...

connect.resume_contact_recording(
    InstanceId=instance_id,
    ContactId=contact_id,
    InitialContactId=initial_contact_id,
)

The suspended period is replaced with silence in the final recording, so a downstream listener cannot reconstruct the digits.

2. Pattern A — DTMF capture with Store customer input (cleanest fit for digits)

This is the Connect-native pattern and is the cleanest fit for PCI cardholder data capture. The caller enters digits on the phone keypad; Connect encrypts the digits with a public-key certificate you have uploaded to the instance, and the ciphertext is stored as a contact attribute.

Setup:

  1. Generate an RSA 2048+ key pair. Keep the private key safe (for example in AWS Secrets Manager or on the payment processor that will decrypt) — never in the Connect account.
  2. Upload the public-key X.509 certificate to your Amazon Connect instance under the instance's encryption-key settings. Connect uses this certificate to encrypt customer input. (The exact UI label for this section may vary by region — look for the section in your instance's settings that lets you add encryption keys / certificates for Connect to use.)
  3. In the contact flow, add a Store customer input block. Note that this block is voice-only, runs in the IVR before agent connect, and supports up to 20 digits. The relevant properties are:
    • Maximum Digits — set to the digit length you need (16 for most PANs, 3–4 for CVV, up to 19 for some 19-digit PANs).
    • Timeout before first entry / Timeout in between each entry — give the caller realistic time to read and enter.
    • Encrypt entry — toggle on. You will then be prompted to choose the Encryption key ID that corresponds to the public-key certificate you uploaded in step 2.
    • Specify terminating keypress — for example #, or set Maximum Digits to a fixed length and let it auto-terminate.
  4. After the block, the encrypted value is available via the $.StoredCustomerInput system attribute. Pass it through to a payment-processor Lambda using an Invoke AWS Lambda function block.

What you get on the wire:

  • Contact-flow logs do not include the digits.
  • Only ciphertext (or a contact-attribute pointer to it) flows into the CTR. Note that Stored customer input is a system attribute and does not appear in the CTR by default — if you copy it to a contact attribute or stream it onward, the ciphertext travels with that downstream pipeline (Kinesis, S3, etc.). Treat that ciphertext as PCI-relevant only inside its decrypt boundary.
  • The recording is suspended around the capture (per Section 1), so DTMF tones are not captured in audio.
  • Only the holder of the matching RSA private key can decrypt — typically your payment processor in a PCI-scope account.

3. Pattern B — Lex + Lambda + AWS KMS (for spoken or natural-language input)

When the input is spoken (the caller reads digits aloud) or is not digits (passphrase, security answer), DTMF capture does not apply. Build an Amazon Lex V2 bot that the contact flow invokes, with a Lambda fulfillment that encrypts the slot value at the moment of capture.

Architecture:

architecture

Lex V2 bot configuration:

  1. Create an intent CollectPayment.
  2. Add a slot CardNumber. For a PAN, prefer either:
    • AMAZON.Number with a length check in the Lambda, or
    • a custom slot type with a regular expression such as ^\d{13,19}$ for strict PAN-shape validation. AMAZON.AlphaNumeric will technically work but is intended for mixed-character identifiers (postal codes, license numbers); it is not the recommended type for a numeric PAN.
  3. Configure the intent with a Fulfillment Lambda hook.
  4. Either disable conversation logs for this bot, or — if you need logs for debugging or other slots — keep logs enabled but mark the CardNumber slot with obfuscationSetting = DEFAULT_OBFUSCATION, which masks that slot's value in CloudWatch logs while leaving other turn metadata visible.
  5. In the Connect flow, use Get customer input with the Lex bot, after the recording-suspend block from Section 1.

Lambda fulfillment (Python, AWS Encryption SDK v4.x with the Material Providers Library):

import os
import base64
import boto3

import aws_encryption_sdk
from aws_encryption_sdk import CommitmentPolicy
from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
from aws_cryptographic_material_providers.mpl.models import CreateAwsKmsKeyringInput

KMS_KEY_ARN = os.environ["KMS_KEY_ARN"]

_esdk_client = aws_encryption_sdk.EncryptionSDKClient(
    commitment_policy=CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT
)

_mpl = AwsCryptographicMaterialProviders(MaterialProvidersConfig())
_kms_client = boto3.client("kms")
_kms_keyring = _mpl.create_aws_kms_keyring(
    input=CreateAwsKmsKeyringInput(
        kms_key_id=KMS_KEY_ARN,
        kms_client=_kms_client,
    )
)

def lambda_handler(event, _context):
    intent = event["sessionState"]["intent"]
    slots = intent["slots"]
    card_number = slots["CardNumber"]["value"]["interpretedValue"]

    ciphertext, _header = _esdk_client.encrypt(
        source=card_number.encode("utf-8"),
        keyring=_kms_keyring,
        encryption_context={
            "purpose": "connect-secure-input",
            "intent": intent["name"],
        },
    )
    token = base64.b64encode(ciphertext).decode("ascii")

    return {
        "sessionState": {
            "dialogAction": {"type": "Close"},
            "intent": {
                "name": intent["name"],
                "state": "Fulfilled",
                "slots": slots,
            },
            "sessionAttributes": {
                "encryptedCardToken": token,
            },
        },
        "messages": [
            {
                "contentType": "PlainText",
                "content": "Thank you. Your payment information has been securely captured.",
            }
        ],
    }

A few points that matter:

  • AWS Encryption SDK v4.x uses keyrings via the Material Providers Library (AwsCryptographicMaterialProviders). Each MPL method takes a single input= dataclass — that pattern is required for v4.x and replaces the legacy StrictAwsKmsMasterKeyProvider constructor style.
  • Encryption context binds each ciphertext to its purpose. KMS records the context in CloudTrail, and the key policy condition shown in Section 4 means a token leaked into another flow without the matching context cannot be decrypted.
  • Never log the slot value. The handler does not print, log, or persist card_number.
  • CloudWatch log retention. PCI DSS Requirement 10.5.1 calls for at least one year of audit-log retention (with at least three months immediately available). Do not set the Lambda log retention shorter than your PCI retention requirement; instead, audit the log group to confirm slot values are not being written, route logs to a KMS-encrypted, access-restricted log archive (CloudWatch Logs subscription → S3 with Object Lock is a common pattern), and rely on tight IAM and a log-scanning detective control rather than aggressive deletion.
  • Lambda environment variables. KMS_KEY_ARN is non-sensitive (it's a public ARN). Do not put any secret in environment variables; use AWS Secrets Manager if you need credentials.

4. KMS key policy

Encrypt-only for the Lex Lambda role; decrypt-only for the payment-processor role. The Connect SLR is intentionally absent from this key policy — it has no need to decrypt the secure-input ciphertext, and omitting it preserves a clean separation of duties.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowEncryptFromLexLambda",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/ConnectSecureInputLambdaRole"
      },
      "Action": ["kms:GenerateDataKey", "kms:Encrypt"],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "kms:EncryptionContext:purpose": "connect-secure-input"
        }
      }
    },
    {
      "Sid": "AllowDecryptFromPaymentProcessor",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::234567890123:role/PaymentProcessorRole"
      },
      "Action": "kms:Decrypt",
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "kms:EncryptionContext:purpose": "connect-secure-input"
        }
      }
    },
    {
      "Sid": "EnableKeyAdministration",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/KmsAdmin"
      },
      "Action": [
        "kms:Describe*", "kms:Get*", "kms:List*",
        "kms:Update*", "kms:Enable*", "kms:Disable*",
        "kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion"
      ],
      "Resource": "*"
    }
  ]
}

Cross-account is common — the payment processor often runs in a separate PCI-scoped account. For cross-account access, both sides must allow the call: the key policy above grants access from the payment-processor role's account, and the role's IAM policy in that account must additionally grant kms:Decrypt on this CMK ARN. KMS evaluates cross-account requests against both the key policy and the caller's IAM policy.

5. PCI DSS v4.0.1 scope reduction

PCI DSS v4.0 was retired on December 31, 2024 and PCI DSS v4.0.1 (published June 2024) is the active standard, with future-dated requirements becoming mandatory on March 31, 2025. Key requirements relevant to this pattern:

  • Requirement 3.3.1 — Sensitive Authentication Data (SAD), which includes CVV/CVC/CID, must not be retained after authorization completes, even if encrypted.
  • Requirement 3.6 — Cryptographic keys used to protect stored account data must be secured.
  • Requirement 3.7 — Key-management processes (generation, distribution, rotation, retirement, replacement) must be defined and implemented.
  • Requirement 10.5.1 — Audit-log retention for at least one year, with at least three months immediately available.

This pattern reduces scope because:

ComponentIn scope?Why
Connect agents and CCPOutNever observe PAN or CVV
Call recordings (S3)OutRecording suspended during capture
Contact Lens transcriptsOutNo PAN in audio = no PAN in transcript
CTR / contact-flow logsOutOnly ciphertext is referenced (and only if you propagate the encrypted attribute)
Lex Lambda + KMS CMKInHandles plaintext briefly during encrypt
Payment processor + KMS CMKInHandles decrypt and downstream auth

The in-scope components are minimized to two narrow services that an assessor can examine in isolation.

For PCI compliance documentation, capture:

  • The KMS key policy (proves only authorized roles can decrypt).
  • CloudTrail events for the CMK during a representative call (proves encrypt/decrypt logs are intact).
  • The flow JSON showing recording suspend/resume around the capture (proves no recording).
  • A test transcript and CTR showing the absence of the PAN.

6. Validate end-to-end

  1. Place a test call. Use a test PAN such as 4111 1111 1111 1111.
  2. Verify the WAV under call-recordings/ has silence during the capture segment.
  3. Open the redacted (and original, if retained) Contact Lens transcript under Analysis/Voice/. The PAN must not appear.
  4. Open the contact in Contact search in the Connect admin website. The contact attribute encryptedCardToken should be a base64 ciphertext.
  5. In CloudTrail, filter on kms.amazonaws.com and the CMK ARN. Confirm:
    • GenerateDataKey invoked by the Lex Lambda role with encryption context purpose=connect-secure-input.
    • Decrypt invoked only by the payment-processor role.
    • No agent IAM principal appears.

7. Common pitfalls

  • Forgetting to suspend recording. Without the suspend, the Lex prompt's audio of the caller reading the PAN — and any DTMF tones — end up in the recording. Test by listening to the WAV after a full end-to-end run.
  • Lex conversation logs leaking the slot. Either disable conversation logs on the secure-input bot or set obfuscationSetting = DEFAULT_OBFUSCATION on the sensitive slot.
  • CloudWatch Lambda log too verbose. Print statements added during early development can leak the slot. Audit the log group, but balance the audit against PCI Req 10.5.1 retention requirements — do not solve a leak by deleting evidence.
  • Missing encryption context. Without kms:EncryptionContext conditions in the key policy, a token leaked into a different code path could be decrypted by an unrelated caller. Always bind context.
  • Missing the IAM side of cross-account decrypt. Granting decrypt only in the key policy is not enough — the caller's IAM role in the remote account also needs kms:Decrypt on the CMK ARN.
  • Storing CVV / SAD. PCI DSS Req 3.3.1 prohibits retaining SAD after authorization, even when encrypted. Enforce a TTL in the payment processor and confirm the discard step in your assessment.
  • Stored customer input system attribute. This attribute does not flow to the CTR by default. If you copy it to a contact attribute or stream it onward, treat that ciphertext as a PCI-relevant artifact wherever it travels.

Related information