Building a Multi-Lambda Payment Receipt Pipeline for Tenant Management
We built a complete payment logging system for a property management tenant hub, enabling landlords to forward bank notifications directly into an automated receipt pipeline. This post covers the architecture, Lambda functions, authentication patterns, and email integration that makes it work.
The Challenge
A tenant portal at 3028fiftyfirststreet.92105.dangerouscentaur.com needed a way for property managers to log rent payments received via Zelle without manually typing them into a system. The solution had to:
- Accept forwarded bank emails and SMS messages
- Parse payment details from unstructured text
- Validate requests with admin-only authentication
- Persist receipt data to S3
- Display results in the tenant hub UI without page refresh
- Run entirely within the
dangerouscentaur.comdomain (not queenofsandiego.com)
Architecture Overview
We deployed three Lambda functions working in concert:
- receipt-action Lambda — Handles admin commands including
log_rent_payment - lambda-email-parser Lambda — Extracts structured data from forwarded emails
- Google Apps Script — Routes incoming emails to the correct Lambda via HTTP
A Google Workspace inbox alias forwards all payment notifications to a GAS trigger, which parses the content and calls the appropriate Lambda function. Admin tokens authenticate each request.
Lambda Function Details
Receipt-Action Lambda
Located at /Users/cb/Documents/repos/sites/dangerouscentaur/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/scripts/lambda-receipt-action/lambda_function.py
This function handles administrative actions on the receipts dataset. The core pattern:
def lambda_handler(event, context):
# Validate admin token from environment variable
admin_token = os.environ.get('ADMIN_TOKEN')
request_token = event.get('headers', {}).get('Authorization', '')
if not validate_token(request_token, admin_token):
return error_response(403, 'Unauthorized')
action = event.get('action')
if action == 'log_rent_payment':
return log_rent_payment(event)
elif action == 'list_receipts':
return list_receipts()
else:
return error_response(400, 'Unknown action')
The log_rent_payment handler creates a new receipt entry in the S3-backed receipts JSON file, adding timestamps and payment metadata. It accepts:
tenant_id— Identifier matching the user table in the hubamount— Payment amount in dollarspayment_method— "zelle", "check", "transfer", etc.reference— Bank transaction ID or confirmation codenotes— Optional additional context
We store all receipts in a single JSON file in the tenant portal S3 bucket under receipts.json. This is read by the hub UI to populate a receipts section without requiring database infrastructure.
Email Parser Lambda
Created at /Users/cb/Documents/repos/sites/dangerouscentaur/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/scripts/lambda-email-parser/lambda_function.py
This function extracts structured payment data from unstructured email text:
def parse_zelle_email(email_body):
"""Extract Zelle payment details from bank notification"""
patterns = {
'amount': r'\$(\d+(?:,\d{3})*(?:\.\d{2})?)',
'confirmation': r'(?:confirmation|reference|confirmation ID)[:\s]+([A-Z0-9]+)',
'sender': r'(?:From|received from)[:\s]+([^<\n]+)',
'timestamp': r'(?:Date|Time)[:\s]+(.+?)(?:\n|$)'
}
result = {}
for key, pattern in patterns.items():
match = re.search(pattern, email_body, re.IGNORECASE)
if match:
result[key] = match.group(1)
return result
The parser returns a normalized dictionary that the GAS script can use to call the receipt-action Lambda.
Google Apps Script Integration
File: /Users/cb/Documents/repos/sites/queenofsandiego.com/WarmLeadResponder.gs
This GAS project handles inbound emails and routes them to the appropriate handlers. The key addition for payment processing:
function doPost(e) {
const action = e.parameter.action;
const adminToken = PropertiesService.getScriptProperties()
.getProperty('ADMIN_TOKEN');
if (action === 'log_rent_payment') {
return callLambda('receipt-action', {
action: 'log_rent_payment',
tenant_id: e.parameter.tenant_id,
amount: e.parameter.amount,
payment_method: e.parameter.payment_method,
reference: e.parameter.reference,
notes: e.parameter.notes
}, adminToken);
}
}
function callLambda(functionName, payload, token) {
const url = 'https://' + LAMBDA_URL_ID + '.lambda-url.' +
AWS_REGION + '.on.aws/';
const options = {
method: 'post',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(url, options);
return ContentService.createTextOutput(response.getContentText())
.setMimeType(ContentService.MimeType.JSON);
}
Infrastructure & Deployment
S3 & CloudFront
The tenant hub is deployed to an S3 bucket with CloudFront distribution caching. Key resources:
- S3 Bucket — Stores
index.html,receipts.json, and Lambda scripts - CloudFront Distribution — Serves the tenant portal with cache invalidation for rapid updates
- Origin Configuration — S3 origin with OAC (Origin Access Control) for security
After updating Lambda code or credentials, we invalidate CloudFront caches with:
aws cloudfront create-invalidation \
--distribution-id DIST_ID \
--paths "/*"
Lambda Function URLs
Both Lambda functions are exposed via function URLs with resource-based policies. The receipt-action Lambda URL is configured to:
- Accept POST requests with JSON payloads
- Validate Bearer token in Authorization header