Sign in →
Gateways1 min read

AWS API Gateway Integration

Lambda function + CloudWatch log subscription for zero-touch metering. SigV4 authentication. Works with any REST v1 API Gateway endpoint.

Updated 2026-06-15Suggest edits
Docs Gateways AWS API Gateway

Aforo integrates with AWS API Gateway (REST v1) via two complementary paths: a Lambda authorizer for real-time entitlement enforcement, and a CloudWatch Logs subscription filter for asynchronous usage metering. Both are deployed through standard AWS tooling — no agent, no sidecar, no infrastructure change.

aws-api-gateway-metering-architecture.flowLIVE
CLIENT
API Request
HTTPS to API GW
Client calls API Gateway endpoint with X-Tenant-Id and Authorization headers.
0ms
AUTHORIZER
Entitlement
Lambda authorizer
Aforo Lambda checks Redis edge cache for quota + margin. Returns IAM policy.
<5ms
INTEGRATION
Backend
Lambda / HTTP / VPC
API Gateway proxies request to your integration target.
passthrough
CLOUDWATCH
Log Capture
execution logs
API Gateway writes execution log entry to CloudWatch Logs group.
0ms added
FILTER
Subscription
log filter → Lambda
CloudWatch subscription filter triggers the metering Lambda per request log.
2-5s lag
INGEST
Aforo
ingest.aforo.ai
Metering Lambda POSTs enriched usage event. SigV4 signed. Retry on 5xx.
<60ms
aws api gateway → cloudwatch logs → lambda → aforo ingest · sigv4 auth// p95 < 5ms

Prerequisites#

REQUIRED
AWS API Gateway REST v1
HTTP v2 (HTTP API) is not yet supported — use REST API (v1). Check your API type in the AWS Console under API Gateway → APIs.
REQUIRED
Execution logging enabled
API Gateway must have CloudWatch execution logging enabled on the stage. Log level: INFO or ERROR+INFO.
REQUIRED
AWS CLI configured
aws configure with credentials that have IAM and Lambda permissions for deployment.
REQUIRED
Aforo API Key
sk_live_* key from Admin Panel → Settings → API Keys. Stored in AWS Secrets Manager during setup.
OPTIONAL
Node.js 20.x runtime
The Aforo Lambda function targets Node.js 20.x. Ensure your account region supports it.

Lambda Function Setup#

The Aforo metering Lambda receives CloudWatch log events from API Gateway, extracts request metadata, and forwards enriched usage events to the Aforo ingest endpoint. Deploy it from the provided source:

terminal
# Clone the AWS metering Lambda
git clone https://github.com/aforoai/aws-lambda-aforo-metering.git
cd aws-lambda-aforo-metering

# Install dependencies
npm install

# Package for deployment
zip -r aforo-metering.zip index.js node_modules/ package.json

Lambda Handler (index.js)

The core handler decodes CloudWatch log events and forwards them to Aforo. The full source is in the repository; here is the key extraction logic:

index.js
const https = require('https');
const zlib = require('zlib');
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');

const sm = new SecretsManagerClient({ region: process.env.AWS_REGION });

// Cache the Aforo API key for the lifetime of the Lambda execution context
let cachedApiKey = null;

async function getAforoApiKey() {
  if (cachedApiKey) return cachedApiKey;
  const resp = await sm.send(new GetSecretValueCommand({ SecretId: 'aforo/api-key' }));
  cachedApiKey = JSON.parse(resp.SecretString).apiKey;
  return cachedApiKey;
}

exports.handler = async (event) => {
  // Decode and decompress the CloudWatch Logs payload
  const payload = Buffer.from(event.awslogs.data, 'base64');
  const decompressed = zlib.gunzipSync(payload).toString('utf8');
  const logData = JSON.parse(decompressed);

  const apiKey = await getAforoApiKey();
  const usageEvents = [];

  for (const logEvent of logData.logEvents) {
    const parsed = parseApiGatewayLog(logEvent.message);
    if (!parsed) continue;

    usageEvents.push({
      tenantId: parsed.tenantId,           // from X-Tenant-Id header
      productId: parsed.productId,          // from API Gateway stage variable
      metricId: 'api-calls',               // default meter
      quantity: 1,
      timestamp: new Date(logEvent.timestamp).toISOString(),
      metadata: {
        method: parsed.method,
        path: parsed.path,
        statusCode: parsed.status,
        latencyMs: parsed.latency,
        requestId: parsed.requestId,
      },
    });
  }

  if (usageEvents.length === 0) return { statusCode: 200, body: 'no events' };

  // Batch POST to Aforo ingest
  await ingestEvents(apiKey, usageEvents);
  return { statusCode: 200, body: `ingested ${usageEvents.length} events` };
};

function parseApiGatewayLog(message) {
  // API Gateway execution log format:
  // Method: GET, Resource: /search, Stage: prod, ...
  const methodMatch = message.match(/Method: (\w+)/);
  const resourceMatch = message.match(/Resource: ([^,]+)/);
  const statusMatch = message.match(/Status: (\d+)/);
  const latencyMatch = message.match(/Integration latency: (\d+)/);
  const tenantMatch = message.match(/X-Tenant-Id: ([^,\s]+)/);
  const requestIdMatch = message.match(/RequestId: ([a-f0-9-]+)/);

  if (!methodMatch || !tenantMatch) return null;

  return {
    method: methodMatch[1],
    path: resourceMatch?.[1]?.trim() ?? '/',
    status: parseInt(statusMatch?.[1] ?? '200'),
    latency: parseInt(latencyMatch?.[1] ?? '0'),
    tenantId: tenantMatch[1],
    requestId: requestIdMatch?.[1] ?? '',
  };
}

async function ingestEvents(apiKey, events) {
  return new Promise((resolve, reject) => {
    const body = JSON.stringify({ events });
    const options = {
      hostname: 'ingest.aforo.ai',
      path: '/v1/ingest/batch',
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${apiKey}`,
        'Content-Length': Buffer.byteLength(body),
        'X-Idempotency-Key': require('crypto').randomUUID(),
      },
    };

    const req = https.request(options, (res) => {
      res.on('data', () => {});
      res.on('end', resolve);
    });
    req.on('error', reject);
    req.write(body);
    req.end();
  });
}
PRO TIP
The handler caches the API key in the Lambda execution context. Secrets Manager is called only on cold start — warm invocations read from memory. This keeps the hot path under 1ms for secret resolution.

CloudWatch Metering#

API Gateway writes one log entry per request to a CloudWatch Logs group when execution logging is enabled. A subscription filter watches this group and triggers the Aforo Lambda for every new log batch. This is the zero-touch path — no application changes required.

Enable Execution Logging on Your Stage

terminal
# Enable execution logging on the stage (required if not already set)
aws apigateway update-stage \
  --rest-api-id YOUR_API_ID \
  --stage-name prod \
  --patch-operations \
    op=replace,path=/accessLogSettings/destinationArn,value=arn:aws:logs:REGION:ACCOUNT:log-group:API-GW-Execution-Logs_YOUR_API_ID/prod \
    op=replace,path=/*/*/logging/loglevel,value=INFO \
    op=replace,path=/*/*/logging/dataTrace,value=true

# Note the log group name — it follows this pattern:
echo "Log group: API-Gateway-Execution-Logs_YOUR_API_ID/prod"

Create the CloudWatch Subscription Filter

terminal
# Get the Lambda ARN from the previous deploy step
LAMBDA_ARN=$(aws lambda get-function --function-name aforo-metering-aws   --query 'Configuration.FunctionArn' --output text)

# Add permission for CloudWatch to invoke the Lambda
aws lambda add-permission \
  --function-name aforo-metering-aws \
  --statement-id cloudwatch-subscription \
  --action lambda:InvokeFunction \
  --principal logs.amazonaws.com \
  --source-arn arn:aws:logs:REGION:ACCOUNT:log-group:API-Gateway-Execution-Logs_YOUR_API_ID/prod:*

# Create the subscription filter
aws logs put-subscription-filter \
  --log-group-name "API-Gateway-Execution-Logs_YOUR_API_ID/prod" \
  --filter-name "aforo-metering" \
  --filter-pattern "[method, resource, stage, status, latency, integrationStatus, ...]" \
  --destination-arn "$LAMBDA_ARN"
INFO
The subscription filter pattern [method, resource, ...] matches structured API Gateway execution logs. CloudWatch batches log events and delivers them to the Lambda within 2-5 seconds of the original request — usage appears in Aforo with a short lag, not in real time. Billing is unaffected: events are timestamped to the original request time.

IAM Role Configuration#

The Aforo Lambda function requires a minimal IAM execution role. Apply the principle of least privilege — the role needs only CloudWatch Logs write access and Secrets Manager read access for the Aforo API key.

IAM Policy (aforo-metering-lambda-policy.json)

aforo-metering-lambda-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "CloudWatchLogs",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:log-group:/aws/lambda/aforo-metering-aws:*"
    },
    {
      "Sid": "SecretsManagerReadAforoKey",
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue"
      ],
      "Resource": "arn:aws:secretsmanager:REGION:ACCOUNT:secret:aforo/api-key-*"
    },
    {
      "Sid": "XRayTracing",
      "Effect": "Allow",
      "Action": [
        "xray:PutTraceSegments",
        "xray:PutTelemetryRecords"
      ],
      "Resource": "*"
    }
  ]
}

Create the Role via AWS CLI

terminal
# Create the trust policy for Lambda
cat > trust-policy.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "lambda.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}
EOF

# Create the role
aws iam create-role \
  --role-name aforo-metering-lambda-role \
  --assume-role-policy-document file://trust-policy.json

# Attach the custom policy
aws iam put-role-policy \
  --role-name aforo-metering-lambda-role \
  --policy-name aforo-metering-permissions \
  --policy-document file://aforo-metering-lambda-policy.json

# Store the Aforo API key in Secrets Manager
aws secretsmanager create-secret \
  --name "aforo/api-key" \
  --secret-string '{"apiKey":"sk_live_your_key_here"}'
WARNING
Never hardcode the Aforo API key in the Lambda source or environment variables. Secrets Manager with the IAM policy above is the correct approach — the key is encrypted at rest, audited on every access, and rotatable without a Lambda redeploy.

Deploy & Verify#

Deploy the Lambda function and run a verification request through API Gateway:

Deploy the Lambda

terminal
# Get the role ARN
ROLE_ARN=$(aws iam get-role --role-name aforo-metering-lambda-role   --query 'Role.Arn' --output text)

# Create the Lambda function
aws lambda create-function \
  --function-name aforo-metering-aws \
  --runtime nodejs20.x \
  --handler index.handler \
  --role "$ROLE_ARN" \
  --zip-file fileb://aforo-metering.zip \
  --timeout 30 \
  --memory-size 256 \
  --environment Variables="{AWS_REGION=$(aws configure get region)}" \
  --tracing-config Mode=Active

# Verify function is active
aws lambda get-function-configuration \
  --function-name aforo-metering-aws \
  --query '{State: State, Runtime: Runtime, MemorySize: MemorySize}'

Send a Test Request

terminal
# Make a request through your API Gateway stage
curl -v "https://YOUR_API_ID.execute-api.REGION.amazonaws.com/prod/your-endpoint" \
  -H "X-Tenant-Id: test_tenant_123" \
  -H "Authorization: Bearer your_customer_api_key"

# Check Lambda logs for successful event forwarding
aws logs tail /aws/lambda/aforo-metering-aws --follow --format short

# Expected log output:
# ingested 1 events
# POST https://ingest.aforo.ai/v1/ingest/batch → 200 OK

# Verify the event arrived in Aforo
curl -s "https://api.aforo.ai/v1/events?tenant_id=test_tenant_123&limit=1" \
  -H "Authorization: Bearer sk_live_your_admin_key" | jq .
TROUBLESHOOTING
No Lambda invocations: Verify the CloudWatch subscription filter is attached to the correct log group. Check with: aws logs describe-subscription-filters --log-group-name "..."
Lambda invoked but no Aforo events: Check Lambda logs for parse failures. API Gateway log format varies by logging level — ensure level=INFO and data trace=true.
SecretsManager access denied: Verify the IAM role ARN in the Lambda config matches the role that has the SecretsManager policy attached.
Missing X-Tenant-Id: API Gateway must pass the header through to execution logs. Enable full request logging under Stage → Logs/Tracing → Data Trace.
PRO TIP
CloudWatch subscription filters have a 2-5 second delivery lag. This is expected — usage events appear in Aforo a few seconds after the API request completes. Real-time dashboards in Aforo account for this lag automatically. The event timestamp is set to the original request time, not the delivery time, so billing calculations are always accurate.