AI Support Triage API on AWS
Build a protected AWS API that classifies support messages with Amazon Bedrock.
Introduction
30 Second Summary
Support teams often face a queue of messages that all look urgent at first glance. Manual sorting slows the first response.
In this project, you will build a support-triage API that turns each customer message into a constrained category plus concise summary. Amazon API Gateway protects the AWS Lambda endpoint that calls Amazon Bedrock.
What You'll Build
Your Windows PowerShell demo shows a protected endpoint turning a customer message into a structured category plus concise summary.
By the end of this project, you'll have:
- A live triage call that turns a support message into a constrained category plus concise summary.
- A failure-path demonstration where malformed input receives a gateway-owned error while a missing API key receives a 403 response.
- A reviewable demo package that pairs Amazon CloudWatch Logs evidence with an honest README.md.
- Secret Mission: Extend the model result with a constrained LOW, MEDIUM, or HIGH priority for support routing.
Are there any prerequisites?
You need a Windows computer with permission to create AWS resources. Step 1 includes the AWS account checkpoint if your account is not ready.
Before We Start
Before We Start...
Before any hands-on work begins, this checkpoint locks in what your support-triage API must prove. You are committing to constrained AI classification behind an Amazon API Gateway REST API with request validation, traffic controls, and observability boundaries.
Get Ready for AWS
Before you create cloud resources, you need a predictable place to work. This step confirms that your AWS account, Region, Windows PowerShell, and browser shell are ready.
You will use AWS CloudShell for Linux packaging commands. You will use Windows PowerShell for the API tests that follow.
In this step, get ready to:
- Confirm access to your AWS account in us-east-1.
- Check that Windows PowerShell can run the local test commands.
- Verify the CloudShell toolchain used to package the Lambda function.
Confirm your AWS account and Region
- Open the AWS Management Console in your browser.
- Sign in to the account you want to use for this project.
- Open the Region selector in the top navigation bar.
- Select US East (N. Virginia) us-east-1.
You should see N. Virginia in the console header. This keeps the Lambda function and Amazon Bedrock model in the same Region.
Check Windows PowerShell
- Press the Windows key to open the search bar.
- Type PowerShell.
- Press Enter to open Windows PowerShell.
- Display the installed PowerShell version by running this command:
$PSVersionTable.PSVersion
You should see a version table. PowerShell 5.1 or later supports the REST and JSON commands used in this project.
PowerShell did not open?
Search for Windows PowerShell instead of PowerShell. Help me open PowerShell on Windows.
Check AWS CloudShell
- Return to the AWS Management Console.
- Click the CloudShell icon in the top navigation bar.
- Wait for the prompt to load. The first session can take about a minute while AWS prepares the environment.
- Check the packaging tools by running this command:
aws --version && python3 --version && pip3 --version && zip --version | head -n 1
You should see version details for the AWS CLI, Python, pip, and zip. That output confirms the browser shell has every packaging tool you need.
CloudShell did not start?
Confirm that the console still shows N. Virginia. Then refresh the browser and open CloudShell again. Help me troubleshoot AWS CloudShell startup.
Your account and command-line tools are ready. Next, you will create the permissions and Lambda function that give the classifier a secure place to run.
Set Up the AWS Foundations
Your account and tools are ready. The classifier now needs a Lambda runtime with permission to call Amazon Bedrock.
An IAM execution role controls what the function can do. You will start with logging permission, then add only the model-invocation action required by the classifier.
In this step, get ready to:
- Create the Bedrock permission policy in CloudShell.
- Create the Python Lambda function and its execution role.
- Run a private Lambda test that returns a visible health response.
Create the Bedrock policy
The classifier calls the Bedrock Converse API. Its execution role therefore needs the verified bedrock:InvokeModel action.
- Return to your CloudShell prompt in us-east-1.
- Open bedrock-policy.json in the CloudShell editor by running this command:
edit bedrock-policy.json
- Replace the editor contents with this policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "InvokeFoundationModels",
"Effect": "Allow",
"Action": "bedrock:InvokeModel",
"Resource": "arn:aws:bedrock:*::foundation-model/*"
}
]
}
What does this policy allow?
- The Action grants model invocation through Bedrock Runtime.
- The foundation-model resource pattern covers the AWS-managed model used in this project.
- Press Ctrl+S to save bedrock-policy.json.
- Close the editor tab.
- Validate the saved JSON by running this command:
python3 -m json.tool bedrock-policy.json
You should see the formatted policy with no error message. That output proves the file is valid JSON and persisted in CloudShell.
JSON validation failed?
Reopen bedrock-policy.json and check every quote and comma. Help me fix this policy JSON.
✔️ Awesome, I've got everything!
Your saved policy is valid and ready to attach to the Lambda role.
ⓧ I'd like to double check the full code
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "InvokeFoundationModels",
"Effect": "Allow",
"Action": "bedrock:InvokeModel",
"Resource": "arn:aws:bedrock:*::foundation-model/*"
}
]
}
Create the Lambda function
- Open the Lambda console from the AWS Management Console search box.
- Choose Create function.
- Choose Author from scratch.
- Enter ai-support-triage-api in the Function name field.
- Select Python 3.13 for Runtime.
- Keep Create a new role with basic Lambda permissions selected.
- Choose Create function.
Expect Lambda to spend a few seconds provisioning the function. You will see its code page when the runtime and logging role are ready.
- Open the Configuration tab.
- Choose Permissions.
- Open the role listed under Execution role.
- Choose Add permissions.
- Choose Create inline policy.
- Choose the JSON editor.
- Paste the contents of bedrock-policy.json into the editor.
- Choose Next.
- Enter BedrockInvokeModel in the Policy name field.
- Choose Create policy.
You should see BedrockInvokeModel in the role's permissions list. The function can now write logs and invoke a Bedrock foundation model.
Policy missing from the role?
Confirm that you created the inline policy from the exact role linked on the Lambda function's Permissions page. Help me find the correct execution role.
Test the runtime
- Return to the ai-support-triage-api Lambda function.
- Open the Code tab.
- Select lambda_function.py in the code editor.
- Replace the starter code with this temporary handler:
def lambda_handler(event, context):
# Prove the runtime works before adding the classifier
return {
"statusCode": 200,
"body": "setup-ready",
}
What does this handler do?
- The lambda_handler function is the entry point Lambda invokes.
- The response gives you a visible health signal before any model logic is added.
- Choose Deploy to save the function code.
- Choose Test.
- Create a test event named setup-ready if Lambda prompts you.
- Keep the generated JSON event unchanged.
Before you run the test, do you expect the temporary handler to return a successful response?
- Choose Test to invoke the function.
The Execution result should show statusCode as 200 and a body of setup-ready.
Test did not return 200?
Confirm that the runtime is Python 3.13 and the handler remains lambda_function.lambda_handler. Help me debug this Lambda test.
✔️ Awesome, I've got everything!
Your temporary handler is deployed and returning the expected response.
ⓧ I'd like to double check the full code
def lambda_handler(event, context):
# Prove the runtime works before adding the classifier
return {
"statusCode": 200,
"body": "setup-ready",
}
Your Lambda foundation now accepts private invocations and has permission to call Bedrock. Next, you will replace the health response with the AI triage classifier.
Build the AI Triage Classifier
Your AWS Lambda function now runs with the permissions it needs. The temporary response proves the foundation works.
The current handler cannot classify a support message. It also cannot guarantee a machine-readable JSON result. You will replace it with a classifier that uses Amazon Bedrock, Amazon Nova Micro, and a pinned Boto3 dependency.
In this step, get ready to:
- Create the classifier code with a constrained model tool.
- Package the pinned dependency inside a ZIP deployment package.
- Deploy the package and verify structured classifications through direct Lambda tests.
Create the classifier files
Amazon Nova Micro supports client-side tool calling. The function forces one tool call so the model returns a category plus a summary in a predictable structure.
Why force a tool call?
Amazon Nova Micro does not support structured outputs. A forced tool call gives the model a schema containing the allowed categories.
The function still validates the returned values before sending them to the caller. This keeps unexpected model output outside the successful response path.
- Switch back to the AWS CloudShell session from the previous step.
- Create lambda_function.py in your CloudShell home directory by running this command:
edit lambda_function.py
What does this command do?
The edit command opens lambda_function.py in CloudShell's visual editor. The file stays in your region-scoped CloudShell storage.
- Replace the editor contents with this classifier:
import json
import boto3
# Reuse one Bedrock client across warm Lambda invocations
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
ALLOWED_CATEGORIES = {"BILLING", "TECHNICAL", "ACCOUNT_ACCESS", "FEATURE_REQUEST", "OTHER"}
TRIAGE_TOOL = {"tools": [{"toolSpec": {"name": "classify_support_request", "description": "Classify one support request.", "inputSchema": {"json": {"type": "object", "properties": {"category": {"type": "string", "enum": sorted(ALLOWED_CATEGORIES)}, "summary": {"type": "string", "maxLength": 140}}, "required": ["category", "summary"]}}}}], "toolChoice": {"tool": {"name": "classify_support_request"}}}
def respond(status_code, payload):
# Return the proxy shape expected by Lambda and API Gateway
return {"statusCode": status_code, "headers": {"Content-Type": "application/json"}, "body": json.dumps(payload)}
def lambda_handler(event, context):
# Accept direct Lambda events and API Gateway proxy events
try:
raw_body = event.get("body", event)
body = json.loads(raw_body) if isinstance(raw_body, str) else raw_body
message = body.get("message", "").strip()
if not message:
return respond(400, {"error": "message is required"})
result = bedrock.converse(modelId="amazon.nova-micro-v1:0", system=[{"text": "Classify the support request with the required tool."}], messages=[{"role": "user", "content": [{"text": message}]}], inferenceConfig={"temperature": 0}, toolConfig=TRIAGE_TOOL)
tool_use = next(item["toolUse"] for item in result["output"]["message"]["content"] if "toolUse" in item)
triage = tool_use["input"]
if triage.get("category") not in ALLOWED_CATEGORIES or not triage.get("summary"):
raise ValueError("invalid model output")
print(json.dumps({"event": "triage_completed", "category": triage["category"]}))
return respond(200, triage)
except Exception as error:
print(json.dumps({"event": "triage_failed", "errorType": type(error).__name__}))
return respond(500, {"error": "classification failed"})
What does this classifier do?
- The Bedrock client targets us-east-1 and reuses its connection across warm invocations.
- The forced tool schema limits the model to five categories and a short summary.
- The handler accepts both direct Lambda events and API Gateway proxy events.
- The response and log paths avoid recording the original customer message.
- Save lambda_function.py with Ctrl+S.
- Close the lambda_function.py editor tab.
- Check the Python syntax by running this command:
python3 -m py_compile lambda_function.py
What does this check prove?
Python compiles lambda_function.py without invoking the handler. A successful check returns you to the CloudShell prompt without a syntax error.
Your classifier now passes its first local check. That catches formatting mistakes before they reach Lambda.
✔️ Awesome, I've got everything!
Your classifier file is saved and passes Python's syntax check.
ⓧ I'd like to double check the full code
import json
import boto3
# Reuse one Bedrock client across warm Lambda invocations
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
ALLOWED_CATEGORIES = {"BILLING", "TECHNICAL", "ACCOUNT_ACCESS", "FEATURE_REQUEST", "OTHER"}
TRIAGE_TOOL = {"tools": [{"toolSpec": {"name": "classify_support_request", "description": "Classify one support request.", "inputSchema": {"json": {"type": "object", "properties": {"category": {"type": "string", "enum": sorted(ALLOWED_CATEGORIES)}, "summary": {"type": "string", "maxLength": 140}}, "required": ["category", "summary"]}}}}], "toolChoice": {"tool": {"name": "classify_support_request"}}}
def respond(status_code, payload):
# Return the proxy shape expected by Lambda and API Gateway
return {"statusCode": status_code, "headers": {"Content-Type": "application/json"}, "body": json.dumps(payload)}
def lambda_handler(event, context):
# Accept direct Lambda events and API Gateway proxy events
try:
raw_body = event.get("body", event)
body = json.loads(raw_body) if isinstance(raw_body, str) else raw_body
message = body.get("message", "").strip()
if not message:
return respond(400, {"error": "message is required"})
result = bedrock.converse(modelId="amazon.nova-micro-v1:0", system=[{"text": "Classify the support request with the required tool."}], messages=[{"role": "user", "content": [{"text": message}]}], inferenceConfig={"temperature": 0}, toolConfig=TRIAGE_TOOL)
tool_use = next(item["toolUse"] for item in result["output"]["message"]["content"] if "toolUse" in item)
triage = tool_use["input"]
if triage.get("category") not in ALLOWED_CATEGORIES or not triage.get("summary"):
raise ValueError("invalid model output")
print(json.dumps({"event": "triage_completed", "category": triage["category"]}))
return respond(200, triage)
except Exception as error:
print(json.dumps({"event": "triage_failed", "errorType": type(error).__name__}))
return respond(500, {"error": "classification failed"})
- Create requirements.txt in the same CloudShell home directory by running this command:
edit requirements.txt
What does this command open?
CloudShell opens a dependency manifest named requirements.txt. This file records the exact Boto3 version included in the deployment package.
- Paste this dependency pin into requirements.txt:
boto3==1.43.111
Why pin Boto3?
The 1.43.111 pin makes the packaged SDK version reproducible. Lambda uses this bundled copy before its periodically updated runtime copy.
- Save requirements.txt with Ctrl+S.
- Close the requirements.txt editor tab.
Package and deploy the function
Lambda needs the handler and its dependencies at the root of the deployment archive. Building the package in CloudShell also keeps the dependency files compatible with the Lambda environment.
- Create the package directory and install the pinned dependency into it by running these commands:
mkdir package
python3 -m pip install --target ./package -r requirements.txt
What do these commands do?
- The mkdir command creates a staging directory for the dependency files.
- The pip command reads requirements.txt. It installs the pinned Boto3 package inside package.
Dependency installation failed?
Confirm that requirements.txt is saved in the same directory where you ran the command. Check that its only line matches the dependency pin exactly.
Ask for help with the installation output: Help me diagnose this Boto3 installation failure in AWS CloudShell.
The terminal lists dependency files as it installs them. When the prompt returns without an installation error, the staging directory is ready.
- Build function.zip with the dependency files and handler at its root by running these commands:
cd package
zip -r ../function.zip .
cd ..
zip function.zip lambda_function.py
How is the archive structured?
The first zip command archives the contents of package from inside that directory. This places the installed libraries at the archive root.
The final command adds lambda_function.py at the same root level. Lambda can now locate lambda_function.lambda_handler.
- Deploy function.zip to the existing function by running this command:
aws lambda update-function-code --function-name ai-support-triage-api --zip-file fileb://function.zip
What does this deployment change?
The AWS CLI uploads function.zip to the existing ai-support-triage-api function. The function keeps its current runtime and execution role.
Deployment command failed?
Confirm that the CloudShell prompt is back in the directory containing function.zip. Check that the function name is ai-support-triage-api.
Ask for help with the deployment output: Help me diagnose this Lambda ZIP deployment error.
The command returns a function configuration response naming ai-support-triage-api. Your temporary setup handler has now been replaced by the packaged classifier.
Test the constrained result
A successful deployment only proves that Lambda accepted the archive. A direct test now checks the Bedrock permission and the model's constrained response in one invocation.
- Return to the ai-support-triage-api function page in the AWS Management Console from earlier.
- Select Test on the function page.
- Enter billing-test if Lambda asks for an event name.
- Replace the test event JSON with this message:
{"message":"I was charged twice for my subscription."}
What does this event test?
The event passes a non-empty message directly to lambda_handler(). The classifier should map the duplicate charge to the constrained billing category.
Before you run the test, do you expect the model to return free-form prose or the schema-controlled category and summary?
- Run the event by selecting Test.
You should see Execution result report statusCode as 200. The JSON body contains category BILLING plus a concise summary.
- Repeat the direct test with two customer support messages from different categories.
- Expand the log output inside Execution result.
You should see a log entry containing {"event": "triage_completed", "category": "BILLING"}. The original customer message does not appear in that application log entry.
Seeing a classification error?
A 500 response means the classifier reached its protected failure path. Confirm that the execution role still has the inline Bedrock policy from the previous step.
Confirm that the function remains in us-east-1. The code creates its Bedrock client in that region.
Ask for help without sharing credentials: Help me diagnose why my Lambda classifier returns 500.
That is the classifier working end to end. Your Lambda function now turns a support message into a constrained category plus a concise summary.
Next, you will expose the classifier through a REST endpoint and watch malformed input reveal the first production weak spot.
Expose the Triage API
Your classifier already turns direct AWS Lambda test events into constrained categories plus summaries. A client still needs a stable HTTPS route to reach it.
Amazon API Gateway gives the function a client-facing REST API endpoint. Testing this first deployment reveals whether malformed JSON is stopped at the gateway or passed to the backend.
In this step, get ready to:
- Create a Regional REST API with a /triage resource plus a POST Lambda proxy integration.
- Deploy the API to a prod stage.
- Compare valid and malformed requests from Windows PowerShell.
Create the REST API resource
A resource gives part of the URL its own API behavior. The /triage resource becomes the address where clients submit support messages.
Why Use a REST API?
- API Gateway REST APIs support request validation plus custom gateway responses.
- REST APIs support API keys plus per-client throttling.
- This project uses those controls in later steps.
- HTTP APIs do not provide this same feature set.
- Switch back to the AWS Management Console from earlier.
- Open API Gateway from the console search bar.
- Under REST API, choose Build.
The REST API creation form opens. This form defines the public API container before you add routes.
- Enter ai-support-triage-api in API name.
- Keep API endpoint type set to Regional.
The form now identifies a Regional API named ai-support-triage-api.
- Choose Create API.
- Choose Create resource from the Resources pane.
The resource form opens beneath the root path. The root path is shown as /.
- Keep Proxy resource turned off.
- Keep Resource path set to /.
- Enter triage in Resource name.
The resource preview now shows the full path as /triage.
- Keep CORS turned off.
- Choose Create resource.
You will see /triage nested below the root resource. The API now has the route that clients need.
Deploy the Lambda-backed route
A Lambda proxy integration forwards the request details to your existing function. It also returns the function's proxy response through API Gateway.
- Select the /triage resource.
- Choose Create method.
The method form opens for /triage.
- Select POST for Method type.
- Select Lambda function for Integration type.
The integration settings now show the controls for connecting your Lambda function.
- Turn on Lambda proxy integration.
- Confirm the selected AWS Region is us-east-1.
The console may request permission for API Gateway to invoke your function. That permission applies to this configured integration.
- Enter ai-support-triage-api in Lambda function.
- Choose Create method.
- Approve the invocation permission if the console requests it.
You will see a POST method below /triage. The method points to ai-support-triage-api through the proxy integration.
- Choose Deploy API from the Resources pane.
- Select New stage for Stage.
The deployment form now asks for the stage that receives this snapshot of the API.
- Enter prod in Stage name.
- Choose Deploy.
The prod stage opens with an invoke URL. Your API is now callable outside the console.
- Copy the invoke URL from Stage details.
- Record the invoke URL with /triage appended: https://your-api-id.execute-api.us-east-1.amazonaws.com/prod/triage.
- Press the Windows key to open the Windows search bar.
- Type Windows PowerShell and press Enter.
- Set the URL only for this PowerShell session by running:
$env:TRIAGE_API_URL = "[[TRIAGE_API_URL="https://your-api-id.execute-api.us-east-1.amazonaws.com/prod/triage"]]"
$env:TRIAGE_API_URL
What Does This Command Do?
The first line stores your route in TRIAGE_API_URL for the current PowerShell process. The value disappears when you close this PowerShell window.
The second line prints the stored value. You should see the complete URL ending in /prod/triage.
URL Not Displayed?
- Confirm the command starts with $env:TRIAGE_API_URL.
- Confirm the URL ends with /prod/triage.
Need help checking the session value? Help me troubleshoot TRIAGE_API_URL in Windows PowerShell 5.1.
Test the client-facing behavior
The deployed route now gives PowerShell a client-facing entry point. Start with a valid support message to prove the complete path works.
Before you run this, what category do you expect for a duplicate subscription charge?
- Send the valid support message by running:
$body = @{
message = "I was charged twice for my subscription."
} | ConvertTo-Json -Compress
$response = Invoke-RestMethod `
-Method Post `
-Uri $env:TRIAGE_API_URL `
-ContentType "application/json" `
-Body $body
$response | ConvertTo-Json -Depth 4
What Does This Request Do?
- The first section converts the support message into a compact JSON body.
- The -Method parameter sends a Post request to $env:TRIAGE_API_URL.
- The -ContentType parameter identifies the body as application/json.
- The final line formats the deserialized response for the terminal.
You will see category BILLING plus a concise summary. That confirms the request travelled through API Gateway to Lambda and Bedrock.
No Category in the Response?
- Confirm the printed URL ends with /prod/triage.
- Confirm the deployed prod stage includes the POST method.
- Confirm the method points to ai-support-triage-api in us-east-1.
Still stuck? Help me diagnose my API Gateway POST request.
The valid request works. Now predict which service handles an empty JSON object in this first deployment.
- Send the empty JSON object by running:
$body = "{}"
Invoke-RestMethod `
-Method Post `
-Uri $env:TRIAGE_API_URL `
-ContentType "application/json" `
-Body $body
This Failure Is Intentional
You will see HTTP 400. The response includes {"error":"message is required."}.
That wording comes from lambda_function.py. The empty request reached Lambda before it was rejected.
Seeing a Different Failure?
- Print $env:TRIAGE_API_URL again to confirm the current session still has the route.
- Return to the prod stage to confirm the latest deployment contains POST /triage.
Need help comparing the failure? Help me understand my malformed request response.
Your client-facing route is live. You have also exposed the contract gap that the next step moves into API Gateway.
Validate Requests at the Gateway
Your deployed endpoint handles valid support messages. However, the empty JSON body from the last step still reached AWS Lambda.
The function spent backend work rejecting input that the gateway could recognize. Amazon API Gateway can enforce the request contract before the proxy integration runs.
You will attach a JSON Schema model. You will also customize the gateway-owned validation error.
In this step, get ready to:
- Create the TriageRequest request model.
- Enable body validation for POST /triage.
- Redeploy the prod stage to verify rejected and accepted requests.
Create the request model
A request model describes the JSON shape that clients must send. Your model requires a string property named message.
The model also belongs in your AWS CloudShell project files. This keeps the API contract available for review after the console setup is complete.
- Return to AWS CloudShell from earlier.
- Create request-model.json in the CloudShell editor by running this command:
edit request-model.json
What does this command do?
The edit command opens the named file in CloudShell's visual editor. CloudShell creates the file when it does not already exist.
- Paste this request model into the empty request-model.json editor:
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "TriageRequest",
"type": "object",
"required": ["message"],
"properties": {
"message": {
"type": "string"
}
}
}
What does this model enforce?
- The required array makes message mandatory.
- The properties object defines message as a string.
- The schema identifier tells API Gateway to interpret the model as JSON Schema draft 4.
- Save request-model.json by pressing Ctrl+S.
- Keep the saved file open in the CloudShell editor.
✔️ Awesome, I've got everything!
Your saved request-model.json file is ready to become the gateway contract.
ⓧ I'd like to double check the full code
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "TriageRequest",
"type": "object",
"required": ["message"],
"properties": {
"message": {
"type": "string"
}
}
}
What should match?
Compare every property with your saved file. The model must require message as a string.
- Copy the full contents of request-model.json from the CloudShell editor.
- Return to the API Gateway console from earlier.
- Select the ai-support-triage-api REST API.
- Choose Models in the main navigation pane.
- Choose Create model.
- Enter TriageRequest in the Name field.
- Enter application/json in the Content type field.
- Paste the copied schema into the Model schema field.
- Choose Create model.
You should see TriageRequest in the API's model list. The gateway now has a reusable definition of a valid triage request.
Model creation failing?
- Confirm the model name is exactly TriageRequest.
- Confirm the content type is exactly application/json.
- Compare the braces in the Model schema field with request-model.json.
- Use this if the console still rejects the model: Help me diagnose why API Gateway rejects my JSON Schema model.
Attach validation and customize the error
A model describes the contract. A request validator applies that contract to a specific method before API Gateway invokes Lambda.
- Choose Resources in the API navigation pane.
- Select the /triage resource.
- Select the POST method.
- Open the Method request tab.
- Choose Edit.
Where does validation run?
API Gateway checks the incoming body against the method's model before the integration backend runs. A failed check returns HTTP 400 without invoking Lambda.
- Select Validate body from the Request validator options.
- Expand Request body.
- Choose Add model.
- Enter application/json for Content type.
- Select TriageRequest for Model.
- Choose Save.
The method request should now show body validation with the TriageRequest model for application/json.
The validator owns the rejection path now. Next, you will make that error useful to API clients.
- Choose Gateway responses in the API navigation pane.
- Select BAD_REQUEST_BODY.
- Choose Edit.
- Add an application/json response template.
- Set the response template by pasting this JSON:
{"error":"Request rejected by API Gateway validation."}
What does this response change?
This static body gives every schema validation failure a consistent client-facing message. The response comes from API Gateway because the integration backend does not run.
- Choose Save.
- Confirm BAD_REQUEST_BODY shows status 400 with an application/json template.
Gateway response not saving?
- Confirm you selected BAD_REQUEST_BODY in the gateway responses list.
- Confirm the template content type is application/json.
- Confirm the response body contains valid JSON with matching double quotes.
- Use this if the response still does not save: Help me diagnose my API Gateway BAD_REQUEST_BODY response.
Redeploy and test both request paths
The prod stage still points to the earlier API deployment. Redeploying publishes the model, validator, and custom gateway response to the existing invoke URL.
- Choose Resources in the API navigation pane.
- Choose Deploy API.
- Select prod for Stage.
- Choose Deploy.
Before you rerun the malformed request, do you expect Lambda's earlier error to appear again?
- Switch back to the Windows PowerShell session from earlier.
- Press the Up Arrow key until the malformed POST command with body {} appears.
- Press Enter to send the malformed request.
You should see HTTP 400 with {"error":"Request rejected by API Gateway validation."} in the error details.
What did this prove?
The message now comes from your custom gateway response. API Gateway rejected the missing message property before the request could reach Lambda.
Before you rerun the valid request, do you expect the model to block a body that includes a string message?
- Press the Up Arrow key until the valid support-message POST command from the previous step appears.
- Press Enter to send the valid request.
You should see the constrained category and summary returned by Lambda. The validator has rejected the bad request without breaking the valid path.
Still seeing the Lambda error?
- Redeploy the API to the prod stage again.
- Confirm the request uses application/json as its content type.
- Confirm the method request uses Validate body with TriageRequest.
- Use this if the empty body still reaches Lambda: Help me trace why API Gateway request validation is being bypassed.
That is the contract boundary working: malformed requests stop at the gateway while valid messages still reach your classifier. Next up, you will protect the endpoint and package the finished demo.
Protect and Package the Demo
Your API Gateway contract now rejects malformed JSON before AWS Lambda runs. The endpoint still accepts requests from any client that knows its URL.
This final step adds client metering through an API key. You will package the demo with a Windows PowerShell client plus a README. You will finish by finding evidence of a successful request in Amazon CloudWatch Logs.
In this step, get ready to:
- Require an API key for the deployed triage method.
- Package the protected client workflow in two project files.
- Run the protected demo and confirm its Lambda log entry.
Require an API key and limit traffic
An API key identifies the client sending a request. A usage plan applies throttling targets plus a request quota to that client.
- Switch back to the AWS Management Console from earlier.
- Return to the ai-support-triage-api REST API in API Gateway.
- Select Resources in the API navigation.
- Select the POST method under /triage.
- Select Method request.
- Choose Edit.
- Enable API key required.
- Save the method request.
What changed on the method?
The POST method now expects the client to send a valid key in the X-API-Key header. API Gateway rejects a missing key before invoking Lambda.
- Choose Deploy API from the Deploy API menu.
- Select prod in the Stage list.
- Choose Deploy.
The method protection now applies to calls made through the prod invoke URL.
The API key value is a credential. Keep it outside screenshots plus project files when you reveal it later.
- Select API keys in the API Gateway navigation.
- Choose Create API key.
- Enter ai-support-triage-api in the Name field.
- Select Auto generate for the API key value.
- Choose Save.
- Leave the generated key value hidden until the protected test.
Why leave the value hidden?
The key remains available from its API Gateway details page. You only need to reveal it when you place it in the current PowerShell process.
- Select Usage plans in the API Gateway navigation.
- Choose Create usage plan.
- Enter ai-support-triage-api in the Name field.
- Set Rate to 2 requests per second.
- Set Burst to 5 requests.
- Set Quota to 100 requests.
- Set Period to Day.
- Choose Create usage plan.
What do these limits mean?
The rate sets a target of two requests each second. The burst setting allows a short spike of up to five requests.
The daily quota targets one hundred requests. Usage-plan throttles plus quotas are best-effort traffic controls.
- Choose Add API stage in the usage plan.
- Select ai-support-triage-api in the API list.
- Select prod in the Stage list.
- Choose the confirmation control to add the stage.
- Choose Add API key in the usage plan.
- Select the ai-support-triage-api key.
- Choose the confirmation control to add the key.
Where is the security boundary?
The API key supports client identification plus metering. User authentication requires an identity control such as IAM or an Amazon Cognito user pool.
The usage plan helps manage traffic. AWS Budgets provides cost monitoring when the workload needs a financial safeguard.
A new key association can take a few minutes to propagate. A short delay here means API Gateway is updating the client mapping.
Before you test, do you think the same keyless request from the previous step reaches Lambda?
- Return to the valid Windows PowerShell POST command from the previous step.
- Run the request again without adding an X-API-Key header.
PowerShell reports HTTP 403. API Gateway has blocked the keyless request before Lambda runs.
Still receiving a classification?
Confirm that API key required is enabled on the POST method. Redeploy the REST API to the existing prod stage after saving the change.
Check that PowerShell still uses the prod invoke URL stored in TRIAGE_API_URL.
Ask for help with the protection settings.
Create the client and project README
The demo client reads the invoke URL plus key from the current PowerShell process. The source file stays free of live credentials.
The README records the architecture plus its limits. This gives a reviewer the context behind the working endpoint.
- Return to the Windows PowerShell window from earlier.
- Record the folder path shown between PS and > as your current PowerShell folder.
- Press the Windows key to open search.
- Type Notepad in the search bar.
- Press Enter to open Notepad.
- Paste the parameter plus environment checks below into the blank file:
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateLength(1, 1000)]
[string]$Message
)
if (-not $env:TRIAGE_API_URL) {
throw "Set TRIAGE_API_URL for the current PowerShell session."
}
if (-not $env:TRIAGE_API_KEY) {
throw "Set TRIAGE_API_KEY for the current PowerShell session."
}
What does this code do?
- The parameter requires a support message between one and one thousand characters.
- The first guard stops the script when the current session has no invoke URL.
- The second guard prevents a keyless request from leaving the client.
- Choose File in Notepad.
- Choose Save As.
- Navigate to your current PowerShell folder.
- Enter test-api.ps1 as the file name.
- Select the option that saves all file types.
- Save the file.
The Notepad title now shows test-api.ps1. This confirms the client file exists in the folder used by PowerShell.
Did Notepad add a text extension?
Check the title for test-api.ps1.txt. Use Save As again with the all-files option if that extra extension appears.
Ask for help saving the PowerShell script.
- Place the cursor after the final closing brace in test-api.ps1.
- Paste the protected request logic below:
$headers = @{
"X-API-Key" = $env:TRIAGE_API_KEY
}
$body = @{
message = $Message
} | ConvertTo-Json -Compress
$response = Invoke-RestMethod `
-Method Post `
-Uri $env:TRIAGE_API_URL `
-Headers $headers `
-ContentType "application/json" `
-Body $body
$response | ConvertTo-Json -Depth 4
How does the client send the request?
- The headers table reads the API key from the current PowerShell process.
- The body converts the message into compact JSON.
- The request posts that JSON to the URL stored in the current session.
- The last line prints the structured response for the demo.
- Save test-api.ps1.
- Confirm that the Notepad title shows no unsaved-change indicator.
Is the script missing its request?
Confirm that $headers starts after the second environment guard. Check that every continuation backtick remains at the end of its request line.
Ask for help comparing the client code.
✔️ Awesome, I've got everything!
Your PowerShell client now validates its inputs and sends the API key from the current session.
ⓧ I'd like to double check the full code
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateLength(1, 1000)]
[string]$Message
)
if (-not $env:TRIAGE_API_URL) {
throw "Set TRIAGE_API_URL for the current PowerShell session."
}
if (-not $env:TRIAGE_API_KEY) {
throw "Set TRIAGE_API_KEY for the current PowerShell session."
}
$headers = @{
"X-API-Key" = $env:TRIAGE_API_KEY
}
$body = @{
message = $Message
} | ConvertTo-Json -Compress
$response = Invoke-RestMethod `
-Method Post `
-Uri $env:TRIAGE_API_URL `
-Headers $headers `
-ContentType "application/json" `
-Body $body
$response | ConvertTo-Json -Depth 4
What should match?
Compare every line with your saved test-api.ps1 file. The key comes from TRIAGE_API_KEY instead of a literal value in the script.
- Choose File in Notepad.
- Create a new blank Notepad document.
- Paste the project overview plus demo instructions below:
# AI Support Triage API
A serverless REST API that classifies customer support messages with Amazon Nova Micro.
## Architecture
Windows PowerShell client -> Amazon API Gateway REST API -> AWS Lambda -> Amazon Bedrock
## Demo
Set values only for the current PowerShell session:
```powershell
$env:TRIAGE_API_URL = "https://your-api-id.execute-api.us-east-1.amazonaws.com/prod/triage"
$env:TRIAGE_API_KEY = "your-api-key-here"
./test-api.ps1 -Message "I was charged twice for my subscription."
```
Expected response shape:
```json
{
"category": "BILLING",
"summary": "The customer reports a duplicate subscription charge."
}
```
Remove the key when the demo is finished:
```powershell
Remove-Item Env:TRIAGE_API_KEY
```
What does this section document?
The architecture traces a request from PowerShell through API Gateway plus Lambda to Amazon Bedrock. The demo uses session-only environment variables to keep the live key out of the README.
- Choose File in Notepad.
- Choose Save As.
- Navigate to your current PowerShell folder.
- Enter README.md as the file name.
- Select the option that saves all file types.
- Save the file.
The Notepad title now shows README.md. The saved document contains the architecture plus a repeatable demo workflow.
Did the README become a text file?
Check whether Notepad saved README.md.txt. Save it again with the all-files option if the extra extension appears.
Ask for help saving the Markdown file.
- Place the cursor after the final code fence in README.md.
- Paste the feature list plus security boundary below:
## Production-minded features
- API Gateway REST API with a named `prod` stage
- Lambda proxy integration
- Request validation with a JSON Schema model
- Custom `BAD_REQUEST_BODY` response
- API key and usage plan for client identification and traffic targets
- Pinned Boto3 dependency
- CloudWatch execution logs that omit the original customer message
## Security boundary
The API key supports client identification, quotas, and throttling. It is not user authentication or authorization. A production upgrade should add IAM, an Amazon Cognito user pool, or a Lambda authorizer, then keep the usage plan for metering.
Why document the boundary?
The feature list shows the controls a reviewer can observe. The security section states the role of the API key plus the stronger identity controls a production system needs.
- Save README.md.
- Confirm that the document now shows the Production-minded features heading.
Is the security section missing?
Confirm that the feature list follows the demo code fence. Check that the Security boundary heading appears after the final feature.
Ask for help comparing this README section.
- Place the cursor after the security-boundary paragraph in README.md.
- Paste the production upgrades below:
## Production upgrades
- Define the infrastructure with AWS SAM, AWS CloudFormation, or the AWS CDK.
- Restrict the Lambda role to the exact approved model ARN.
- Add authenticated identities and authorization rules.
- Add AWS WAF and AWS Budgets controls where the workload requires them.
- Add automated tests and a CI/CD deployment workflow.
What do the upgrades show?
The list separates the working demo from future production work. It covers infrastructure as code plus tighter permissions plus identity controls plus cost monitoring plus automated delivery.
- Save README.md.
- Confirm that Production upgrades is the final heading in the document.
Are the upgrades outside the README?
Check that you pasted this section into the existing README.md window. Confirm that Notepad still shows the same file name before saving.
Ask for help checking the final section.
✔️ Awesome, I've got everything!
Your README now explains the working architecture plus the security boundary plus the next production upgrades.
ⓧ I'd like to double check the full code
# AI Support Triage API
A serverless REST API that classifies customer support messages with Amazon Nova Micro.
## Architecture
Windows PowerShell client -> Amazon API Gateway REST API -> AWS Lambda -> Amazon Bedrock
## Demo
Set values only for the current PowerShell session:
```powershell
$env:TRIAGE_API_URL = "https://your-api-id.execute-api.us-east-1.amazonaws.com/prod/triage"
$env:TRIAGE_API_KEY = "your-api-key-here"
./test-api.ps1 -Message "I was charged twice for my subscription."
```
Expected response shape:
```json
{
"category": "BILLING",
"summary": "The customer reports a duplicate subscription charge."
}
```
Remove the key when the demo is finished:
```powershell
Remove-Item Env:TRIAGE_API_KEY
```
## Production-minded features
- API Gateway REST API with a named `prod` stage
- Lambda proxy integration
- Request validation with a JSON Schema model
- Custom `BAD_REQUEST_BODY` response
- API key and usage plan for client identification and traffic targets
- Pinned Boto3 dependency
- CloudWatch execution logs that omit the original customer message
## Security boundary
The API key supports client identification, quotas, and throttling. It is not user authentication or authorization. A production upgrade should add IAM, an Amazon Cognito user pool, or a Lambda authorizer, then keep the usage plan for metering.
## Production upgrades
- Define the infrastructure with AWS SAM, AWS CloudFormation, or the AWS CDK.
- Restrict the Lambda role to the exact approved model ARN.
- Add authenticated identities and authorization rules.
- Add AWS WAF and AWS Budgets controls where the workload requires them.
- Add automated tests and a CI/CD deployment workflow.
What should match?
Compare the full reference with your saved README.md file. Keep the example key placeholder in the README instead of inserting the live credential.
Run the protected demo and inspect logs
The client plus usage plan are ready. The final test proves that a key-bearing request reaches the classifier while the source files remain free of the credential.
The log entry provides backend evidence for the same request. Its event records the category without recording the original customer message.
- Return to API keys in the API Gateway console.
- Select the ai-support-triage-api key.
- Reveal the API key value.
- Copy the revealed value.
You are about to place the credential in one PowerShell process. The value disappears from that process when you remove it at the end of the demo.
- Return to the Windows PowerShell session from earlier.
- Replace your-api-key-here with the copied key in the command below.
- Set the session-only key by running:
$env:TRIAGE_API_KEY = "your-api-key-here"
Where is the key stored?
PowerShell stores this value in the current process environment. The command does not add the credential to test-api.ps1 or README.md.
Before you run the client, do you think the request now passes the API Gateway key check?
- Send a locked-account message through the protected client by running:
./test-api.ps1 -Message "My account is locked"
What should you see?
PowerShell prints a structured result with a constrained category plus a concise summary. The locked-account message should receive the ACCOUNT_ACCESS category.
The successful response proves the API key association has propagated. It also proves the protected route still reaches the classifier.
Did the protected request fail?
Confirm that test-api.ps1 is saved in the folder shown by the current PowerShell prompt. Check that both session environment variables have values.
A new usage-plan association can take a few minutes to propagate. Wait briefly before repeating the request if the copied key is valid.
Ask for help with the protected request.
- Switch back to the AWS Management Console.
- Open the CloudWatch service.
- Select Log groups.
- Select /aws/lambda/ai-support-triage-api.
- Open the newest log stream.
- Locate the latest triage_completed entry.
- Compare its category with the PowerShell response.
The matching category plus timestamp prove that the protected request invoked Lambda. The customer message is absent from the application log entry.
Clearing the environment value only removes the credential from this PowerShell process. The API key remains available for a future demo.
- Remove the session-only key by running:
Remove-Item Env:TRIAGE_API_KEY
What does this remove?
The command removes TRIAGE_API_KEY from the current PowerShell process. It leaves the API Gateway key plus usage plan intact.
Before you check, do you expect PowerShell to print a key value?
- Confirm that the current session no longer contains the key by running:
$env:TRIAGE_API_KEY
What should you see?
PowerShell prints no value. The demo credential has been cleared from the current session.
That completes the production-minded demo. Your endpoint now enforces a client key and records safe execution evidence while your project files stay credential-free.
Secret mission
Add Priority-Aware Routing
Your API can choose the right support queue. Extend its constrained output with an operational priority so a support team can see which issue deserves attention first.
Clean Up Your Resources
Clean Up Your Resources
Your live demo can incur usage-based charges through AWS Lambda, Amazon API Gateway, Amazon Bedrock, and Amazon CloudWatch Logs. Choose Keep, Pause, or Delete based on when you plan to use the endpoint again.
Cost warning
Lambda requests, API Gateway requests, Bedrock inference, and CloudWatch Logs can generate charges. Existing log storage can continue to accrue charges after API activity stops.
Usage-plan throttles and quotas are best-effort traffic controls. AWS billing can continue beyond those targets.
Resources you used:
- The ai-support-triage-api Lambda function and its IAM execution role.
- The ai-support-triage-api REST API, including the prod stage, request model, validator, and gateway response.
- The API key and its associated usage plan.
- The /aws/lambda/ai-support-triage-api CloudWatch log group.
- The source files, package directory, Python cache, and deployment archive stored in CloudShell.
- The local Windows files README.md and test-api.ps1.
Keep everything running
No action is needed. Choose this if you are still actively demonstrating the protected endpoint.
- Keep the API key enabled only for approved client requests.
- Review API Gateway and Bedrock usage after each demonstration.
- Clear the session-only API key from PowerShell when the demonstration ends.
- Retain the CloudShell and Windows project files for future updates.
Pause - I'll come back to this later
Suspend client access while keeping the API, function, logs, and project files available for your return.
- Open API keys in the API Gateway console.
- Select the key associated with ai-support-triage-api.
- Disable the selected key.
- Open Usage plans in the API Gateway console.
- Select the plan associated with the API.
- Remove the prod stage from its associated API stages.
- Confirm that the key shows as disabled.
The access change can take a few minutes to propagate. Stored CloudWatch logs can still incur charges while the project is paused.
Delete - I don't want to use this again
Deletion is permanent. These actions remove every cloud and local resource listed above.
Delete the API resources
- Open Usage plans in the API Gateway console.
- Select the plan associated with ai-support-triage-api.
- Choose Delete to remove the usage plan.
- Open API keys in the API Gateway console.
- Select the key used by the project.
- Choose Delete to remove the API key.
- Open REST APIs in the API Gateway console.
- Select ai-support-triage-api.
- Choose Delete to remove the REST API.
- Confirm that ai-support-triage-api no longer appears in the REST API list.
Delete the Lambda resources
- Open the Lambda console.
- Select the ai-support-triage-api function.
- Choose Actions.
- Choose Delete function.
- Complete the confirmation prompt.
- Confirm that the function no longer appears in the Lambda function list.
- Open Roles in the IAM console.
- Select the execution role created for ai-support-triage-api.
- Confirm that no other workload uses the role.
- Choose Delete to remove the role.
Delete the stored logs
- Open Log groups in the CloudWatch console.
- Select /aws/lambda/ai-support-triage-api.
- Choose Delete log group.
- Complete the confirmation prompt.
- Confirm that the log group no longer appears in the list.
Delete the CloudShell artifacts
- Return to CloudShell in us-east-1.
- Remove the project files and generated directories by running these commands:
rm -rf bedrock-policy.json function.zip lambda_function.py request-model.json requirements.txt package __pycache__
ls -la
You should no longer see the named files, package, or __pycache__ in the listing.
Delete the Windows files
- Open Windows PowerShell.
- Return to your current PowerShell folder and remove both local demo files by running these commands:
Set-Location '[[CURRENT_POWERSHELL_FOLDER="your current PowerShell folder"]]'
Remove-Item .\README.md,.\test-api.ps1 -Force -ErrorAction SilentlyContinue
Test-Path .\README.md,.\test-api.ps1
You should see False twice. That confirms both local files are gone.
Still seeing a project artifact?
Run the matching listing command again and remove only the remaining project item. Help me remove a remaining project file.
The API, compute, permissions, logs, CloudShell artifacts, and Windows demo files are now removed.
Nice Work!
Nice Work!
Outstanding work! You built a production-minded serverless API that turns customer support messages into constrained routing data. Your deployed endpoint now rejects malformed input before the backend runs.
You've learned how to:
- Build a Python 3.13 AWS Lambda classifier that uses Amazon Bedrock with Amazon Nova Micro tool calling. Its return path accepts only validated categories plus concise summaries.
- Expose POST /triage through an Amazon API Gateway REST API. Move malformed-body rejection to a JSON Schema request model with a custom BAD_REQUEST_BODY response.
- Protect client calls with an API key plus a throttled usage plan. Run the secured demo from Windows PowerShell. Confirm safe execution metadata in Amazon CloudWatch Logs. Document the architecture plus its production limitations in README.md.
- Secret Mission: Add priority-aware routing that constrains every result to LOW, MEDIUM, or HIGH. Compare a locked-account message with a feature request to prove the API differentiates urgency.
Ready to quiz yourself?