How do I capture a credit card number in Amazon Connect without storing or exposing it to the agent?
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:
- 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.
- 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.)
- 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.
- After the block, the encrypted value is available via the
$.StoredCustomerInputsystem 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 inputis 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:
Lex V2 bot configuration:
- Create an intent
CollectPayment. - Add a slot
CardNumber. For a PAN, prefer either:AMAZON.Numberwith 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.AlphaNumericwill technically work but is intended for mixed-character identifiers (postal codes, license numbers); it is not the recommended type for a numeric PAN.
- Configure the intent with a Fulfillment Lambda hook.
- Either disable conversation logs for this bot, or — if you need logs for debugging or other slots — keep logs enabled but mark the
CardNumberslot withobfuscationSetting = DEFAULT_OBFUSCATION, which masks that slot's value in CloudWatch logs while leaving other turn metadata visible. - 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 singleinput=dataclass — that pattern is required for v4.x and replaces the legacyStrictAwsKmsMasterKeyProviderconstructor 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_ARNis 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:
| Component | In scope? | Why |
|---|---|---|
| Connect agents and CCP | Out | Never observe PAN or CVV |
| Call recordings (S3) | Out | Recording suspended during capture |
| Contact Lens transcripts | Out | No PAN in audio = no PAN in transcript |
| CTR / contact-flow logs | Out | Only ciphertext is referenced (and only if you propagate the encrypted attribute) |
| Lex Lambda + KMS CMK | In | Handles plaintext briefly during encrypt |
| Payment processor + KMS CMK | In | Handles 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
- Place a test call. Use a test PAN such as
4111 1111 1111 1111. - Verify the WAV under
call-recordings/has silence during the capture segment. - Open the redacted (and original, if retained) Contact Lens transcript under
Analysis/Voice/. The PAN must not appear. - Open the contact in Contact search in the Connect admin website. The contact attribute
encryptedCardTokenshould be a base64 ciphertext. - In CloudTrail, filter on
kms.amazonaws.comand the CMK ARN. Confirm:GenerateDataKeyinvoked by the Lex Lambda role with encryption contextpurpose=connect-secure-input.Decryptinvoked 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_OBFUSCATIONon 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:EncryptionContextconditions 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:Decrypton 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 inputsystem 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
- Amazon Connect: Store customer input flow block
- Set recording, analytics and processing behavior flow block
SuspendContactRecordingAPI andResumeContactRecordingAPI- Best practices for PCI compliance in Amazon Connect
- How to encrypt sensitive caller voice input in Amazon Lex (AWS Security Blog)
- AWS Encryption SDK for Python — example code (v4.x with MPL)
- AWS KMS encryption context
- Amazon Lex V2 — slot obfuscation
- PCI DSS on AWS
