Claude Function Calling with Lambda Function URLs: Building a Secure, Zero‑Config AI Endpoint A developer demonstrates how to build a secure, zero-config AI endpoint by pairing AWS Lambda Function URLs with Claude's function-calling feature. The approach eliminates the need for API Gateway, reducing latency and complexity for single-function AI services. The provided Node.js handler integrates with Claude's API to validate payments and logs audit data to S3. When you need a fast LLM endpoint, the first instinct is to spin up API Gateway—but Lambda Function URLs let you skip that extra hop entirely. Pair them with Claude’s function‑calling feature and you get a self‑contained AI service that’s ready in minutes. Yet most engineers still cling to the old, noisy stack. Imagine you’re sending a postcard. Using API Gateway is like handing the postcard to a postal clerk who stamps it, checks the address, and then hands it to the carrier. It works, but it adds a tiny delay and another place where something can go wrong. Lambda Function URLs are the front‑door key: the postcard goes straight from your hand to the mailbox the Lambda . There’s no middle‑man to configure, no extra cost for a separate service, and the latency is a few milliseconds lower. In plain English:A Lambda Function URL is the quickest, simplest way to expose a Lambda over HTTPS. If you only need one function your Claude proxy you can skip API Gateway entirely. Key takeaway:For a single‑function AI service, Function URLs give you fewer moving parts, lower cost, and a clearer mental model. Claude’s function‑calling feature lets the model suggest a structured action a tool call instead of returning free‑form text. Think of it like a customer asking a clerk for a receipt; the clerk hands back a neatly formatted paper rather than a scribbled note. This structure makes it safe to execute code—your Lambda can trust the JSON shape and act on it. Below is a complete Lambda handler written for Node.js 22 the current LTS version . It: fetch request to Claude’s /v1/chat/completions endpoint, describing a single tool called validatePayment . js // file: src/index.ts import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; // The S3 bucket where we store audit logs. // Replace with your bucket name must already exist . const AUDIT BUCKET = process.env.AUDIT BUCKET ?? "my-ai-audit-bucket"; // Create a single S3 client – it reuses HTTP connections automatically. const s3 = new S3Client {} ; / Lambda entry point. The runtime passes an event object that contains the raw HTTP request when the function is invoked via a Function URL. / export const handler = async event: any = { try { // ---------- 1️⃣ Parse the incoming payment request ---------- // event.body is a JSON string because Function URLs forward the raw body. const requestPayload = JSON.parse event.body ; // Example shape we expect: // { "orderId": "12345", "amountCents": 1999, "currency": "USD", "cardToken": "tok abc" } // ---------- 2️⃣ Call Claude with a tool definition ---------- const claudeResponse = await callClaude requestPayload ; // ---------- 3️⃣ Extract the tool call validatePayment ---------- const toolResult = extractToolResult claudeResponse ; // ---------- 4️⃣ Persist request + response for audit ---------- await persistAudit event.body, JSON.stringify claudeResponse ; // ---------- 5️⃣ Return the tool result to the caller ---------- return { statusCode: 200, headers: { "Content-Type": "application/json", // Simple CORS header – adjust the origin as needed. "Access-Control-Allow-Origin": " ", }, body: JSON.stringify toolResult , }; } catch err: any { // ---------- Error handling ---------- const status = err.statusCode ?? 500; return { statusCode: status, headers: { "Content-Type": "application/json" }, body: JSON.stringify { error: err.message ?? "Internal error" } , }; } }; / Calls Claude’s chat completion endpoint with a single tool called validatePayment . The tool tells Claude how to format a call that we can safely execute. / async function callClaude paymentPayload: any { // Claude API key should be stored in Secrets Manager / Parameter Store. const CLAUDE API KEY = process.env.CLAUDE API KEY ; const CLAUDE URL = "https://api.anthropic.com/v1/chat/completions"; // The tool definition – this tells Claude the JSON schema it must return. const tools = { name: "validatePayment", description: "Check a credit‑card payment request for validity", input schema: { type: "object", properties: { orderId: { type: "string" }, amountCents: { type: "integer" }, currency: { type: "string" }, cardToken: { type: "string" }, }, required: "orderId", "amountCents", "currency", "cardToken" , }, }, ; // Build the request payload for Claude. const body = { model: "claude-3-5-sonnet-20240620", max tokens: 1024, messages: { role: "user", content: "Validate this payment request." }, { role: "assistant", tool calls: }, // placeholder for tool call , tools, // pass our tool definition // We also give Claude the raw payment data so it can decide whether to call the tool. // Claude can also ask follow‑up questions; we keep it simple here. // In a real app you might embed the payload in a system message. // For demo purposes we send it as part of the user message. // Example: // "Here is the payload: { ... }" }; // Add the actual payload to the user content. body.messages 0 as any .content = Payment payload: ${JSON.stringify paymentPayload, } ; const response = await fetch CLAUDE URL, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": CLAUDE API KEY, // Claude expects an explicit version header. "anthropic-version": "2023-06-01", }, body: JSON.stringify body , } ; if response.ok { // Propagate HTTP errors as JavaScript exceptions. const errText = await response.text ; const error = new Error Claude API error ${response.status}: ${errText} ; error as any .statusCode = response.status; throw error; } // Claude returns a JSON structure that includes tool calls when it wants to invoke a function. return await response.json ; } / Pulls the JSON payload out of Claude’s tool call response. Throws a ValidationError if the schema is not respected. / function extractToolResult claudeResponse: any { // The path to tool calls can differ by model version; here we use the standard shape. const toolCalls = claudeResponse?.choices?. 0 ?.message?.tool calls; if toolCalls || toolCalls.length === 0 { throw new Error "Claude did not return a tool call" ; } const call = toolCalls 0 ; if call.name == "validatePayment" { throw new Error Unexpected tool name: ${call.name} ; } // function arguments is a JSON string – parse it. let args: any; try { args = JSON.parse call.function arguments ; } catch { throw new Error "Failed to parse Claude's tool arguments" ; } // Very light validation – in production you would use a JSON schema validator. const required = "orderId", "amountCents", "currency", "cardToken" ; for const key of required { if key in args { throw new Error Missing required field ${key} ; } } // Return the clean, validated object. return { validated: true, details: args }; } / Writes both the raw inbound request and Claude’s raw reply to S3. Using a timestamped key makes it easy to browse audit logs later. / async function persistAudit rawRequest: string, rawResponse: string { const timestamp = new Date .toISOString ; const requestKey = audit/${timestamp} request.json ; const responseKey = audit/${timestamp} claude.json ; // Put the request object. await s3.send new PutObjectCommand { Bucket: AUDIT BUCKET, Key: requestKey, Body: rawRequest, ContentType: "application/json", } , ; // Put the Claude response object. await s3.send new PutObjectCommand { Bucket: AUDIT BUCKET, Key: responseKey, Body: rawResponse, ContentType: "application/json", } , ; } Explanation of the most important lines | Line | What it does | |---|---| event.body | The HTTP payload sent by the caller the payment request . | fetch CLAUDE URL, … | Calls Claude over HTTPS using the built‑in fetch API no extra library needed . | tools | Describes a tool – a function that Claude can ask you to run. The schema tells Claude exactly what keys and types to send back. | tool calls | The place in Claude’s response where the model tells you “I want to run validatePayment with these arguments”. | PutObjectCommand | An AWS SDK command that stores an object a file in an S3 bucket. | process.env.CLAUDE API KEY | Pulls the secret API key from the Lambda’s environment variables – never hard‑code secrets. | Tip:When you first test locally, set process.env.CLAUDE API KEY in a .env file and use the dotenv package. In production you should store the key in AWS Secrets Manager and grant the Lambda read access via an IAM role. Lambda Function URLs default to a 6 KB request body limit. Claude’s tool‑call JSON can easily exceed that when you include rich data e.g., a whole order object . If you forget to raise MaximumPayloadSize via the console or aws lambda update-function-url-config , the runtime silently truncates the body and you’ll see mysterious “invalid JSON” errors. aws lambda update-function-url-config \ --function-name MyClaudeProxy \ --auth-type NONE \ --max-payload-size 64KB In plain English:Think of the default limit as a tiny mailbox slot; you need to ask AWS for a bigger slot before you start dropping larger letters in. An LLM can be instructed to generate code or manipulate data. If anyone on the internet can hit your endpoint, they could flood Claude with malicious prompts, rack up usage charges, or even exfiltrate data from your S3 bucket. IAM authentication gives you a strong, AWS‑native gatekeeper without adding a separate auth layer. When you create the Function URL, set --auth-type AWS IAM . The runtime will then require a signed request SigV4 . A typical front‑end can obtain temporary credentials from Amazon Cognito or from an EC2 instance role. aws lambda create-function-url-config \ --function-name MyClaudeProxy \ --auth-type AWS IAM \ --cors '{"AllowOrigins": "https://myapp.example.com" ,"AllowMethods": "POST" ,"AllowHeaders": "Authorization","Content-Type" }' https://myapp.example.com may call the endpoint, and only POST with the listed headers are allowed. { "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "lambda:InvokeFunctionUrl", "Resource": "arn:aws:lambda:us-east-1:123456789012:function:MyClaudeProxy" } } Attach this policy to the role that your front‑end or another Lambda assumes. Key takeaway:IAM auth + a tight CORS rule gives you a “door with a lock and a peephole” – only callers with proper AWS credentials can open it, and browsers can’t be tricked into sending data from another site. Node 22 introduced native ES modules .mjs by default. If you still use require 'esm' to force CommonJS, the Lambda layer that bundles @aws-sdk/client-lambda silently fails to load, producing a cryptic “Cannot find module” error at cold start. The fix is to switch the file extension to .js and add "type": "module" in package.json , or keep everything CommonJS by naming the file .cjs . Financial operations like payment validation are often subject to compliance rules. Keeping a tamper‑evident log of both the caller’s request and Claude’s exact reply helps you answer “who did what, when”. Sometimes you must redact sensitive fields e.g., cardToken before the log is stored long‑term. S3 Object Lambda lets you attach a small Lambda that transforms the object as it’s being written . aws s3control create-access-point-for-object-lambda \ --name audit-redact-ap \ --region us-east-1 \ --configuration '{ "SupportingAccessPoint": "arn:aws:s3:us-east-1:123456789012:accesspoint/my-audit-bucket-ap", "TransformationConfigurations": { "Actions": "GetObject", "PutObject" , "ContentTransformation": { "AwsLambda": { "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:RedactCardToken" } } } }' Your RedactCardToken Lambda receives the raw object, removes cardToken , and returns the sanitized version to S3. Analogy:Think of an S3 Object Lambda as a security guard at a mailroom who opens every envelope, removes any classified documents, and then reseals it before it goes into storage. js // file: redact.ts export const handler = async event: any = { const original = JSON.parse event.getObjectContext.inputS3Url ; // Delete the sensitive field delete original.cardToken; return { statusCode: 200, headers: { "Content-Type": "application/json" }, body: JSON.stringify original , }; }; Tip:Because the redaction Lambda runs for every put, keep it tiny no external SDKs to avoid extra latency. The Serverless Application Model SAM CLI can invoke a Function URL locally, letting you see the full request/response cycle without deploying. sam local invoke MyClaudeProxy \ -e events/payment-request.json \ --env-vars env.json events/payment-request.json might contain: { "body": "{\"orderId\":\"A100\",\"amountCents\":2500,\"currency\":\"USD\",\"cardToken\":\"tok 123\"}" } If Claude returns a tool calls array but the Lambda logs Claude did not return a tool call , the most common cause is the 6 KB limit mentioned earlier. Check CloudWatch logs for a line like: 2026-08-28T12:34:56.789Z ERROR JSON Parse error: Unexpected end of JSON input That indicates the incoming body was cut off. Verify the Function URL’s MaximumPayloadSize setting. Claude may respond with 400 Bad Request if the tool schema is malformed. Your callClaude helper already throws an error that bubbles up to the top‑level catch . To surface a friendlier message to the caller: js if response.status = 400 && response.status < 500 { const errMsg = await response.text ; const err = new Error Invalid request: ${errMsg} ; err as any .statusCode = response.status; throw err; } Key takeaway:Centralizing error handling lets you map Claude’s HTTP errors to your own API’s error model, keeping the front‑end experience consistent. If you later decide to stream Claude’s response useful for very large payloads , you must set Content-Type: application/octet-stream and add Transfer-Encoding: chunked headers. Omitting those causes the runtime to buffer the entire response, which defeats the purpose of streaming and can hit the 6 KB limit again. What you now have in your toolbox MaximumPayloadSize early in the setup.By stitching these pieces together, you can spin up a production‑grade, self‑contained AI endpoint in under ten minutes—perfect for payment validation, order verification, or any other task where you want the model’s reasoning plus the safety of a typed contract. Happy coding Transparency noticeThis article was written with the help of an AI system — Groq GPT OSS 120B . Published:2026-08-28 ·Primary focus:LambdaAll code blocks are intended to be correct and runnable, but please verify them against the official docs for the tools mentioned before using in production. Find an error? Drop a comment — corrections are always welcome.