Building a Tenant Payment Logging System: Email-to-Payment Automation with AWS Lambda and Google Apps Script
We recently built a complete payment logging workflow for a property management tenant portal. The system allows property managers to forward bank payment notifications (Zelle transfers) directly to a Lambda-powered email handler, which automatically logs the payment and notifies the tenant hub. Here's how we architected it.
The Problem
Previously, manual payment logging required the property manager to type payment information into chat or a form—error-prone and unscalable. We needed a system where forwarding a Zelle payment email would automatically:
- Parse the payment amount and sender
- Log it to a persistent receipts registry
- Update the tenant hub in real-time
- All within the
dangerouscentaur.comdomain (not externally visible)
Architecture Overview
The system comprises four layers:
- Email Ingestion: Amazon SES receiving Zelle forwarded emails at a dedicated
payments@dangerouscentaur.comalias - Message Processing: Lambda function that parses email content and extracts payment data
- Command Dispatch: Google Apps Script detecting payment patterns and calling the logging endpoint
- Data Storage: S3-backed receipts JSON file served to the tenant hub
Infrastructure Setup
SES Domain Verification
We initiated SES domain verification for dangerouscentaur.com via the AWS Console, requesting DKIM tokens to add to the domain's DNS records. This ensures emails sent from payments@dangerouscentaur.com pass SPF and DKIM validation, preventing deliverability issues.
Commands run:
aws ses verify-domain-identity --domain dangerouscentaur.com --region us-east-1
aws ses verify-domain-dkim --domain dangerouscentaur.com --region us-east-1
The DKIM tokens were then added as CNAME records in the domain's DNS provider.
Lambda Functions
Two Lambda functions power the payment system:
- lambda-email-parser (
/scripts/lambda-email-parser/lambda_function.py): Receives raw email from SES, parses headers and body, extracts payment metadata - lambda-receipt-action (
/scripts/lambda-receipt-action/lambda_function.py): Accepts structured payment data via HTTP POST from the Apps Script handler, validates against anADMIN_TOKEN, and writes to S3
Both are deployed with Function URLs enabled (no API Gateway), allowing direct HTTPS invocation with query parameters or request bodies.
S3 and CloudFront
The tenant hub is hosted on S3 behind CloudFront (distribution ID: E2X4A1B2C3D4E5F6G, example). The hub's index.html at /Users/cb/Documents/repos/sites/dangerouscentaur/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/index.html is deployed to S3 at a path like s3://dangerouscentaur-tenant-hubs/3028-51st/index.html.
When payments are logged, we invalidate CloudFront cache patterns to ensure the hub immediately reflects updated receipt data:
aws cloudfront create-invalidation \
--distribution-id E2X4A1B2C3D4E5F6G \
--paths "/*" \
--region us-east-1
The Payment Logging Flow
Step 1: Email Ingestion
The property manager receives a Zelle payment notification email from their bank. They forward it to payments@dangerouscentaur.com.
SES receives the email and triggers a Lambda invocation (configured via SES receipt rule). The lambda-email-parser function extracts:
- Sender email/name (to identify the tenant)
- Amount (via regex on common Zelle patterns: "You sent $X.XX")
- Timestamp (from email headers)
- Reference note (if included in forward subject/body)
Step 2: Parsing and Publishing
The parser Lambda publishes a structured message. This could go to SQS, SNS, or directly to the next step. In this implementation, we chose direct invocation of the receipt-action Lambda via the Python boto3 client:
import boto3
import json
lambda_client = boto3.client('lambda')
response = lambda_client.invoke(
FunctionName='lambda-receipt-action',
InvocationType='RequestResponse',
Payload=json.dumps({
'action': 'log_rent_payment',
'tenant_id': '3028',
'amount': 2500.00,
'method': 'zelle',
'timestamp': '2024-01-15T14:30:00Z',
'admin_token': os.environ['ADMIN_TOKEN']
})
)
Step 3: Receipt Action Handler
The lambda-receipt-action Lambda validates the request:
- Checks that
admin_tokenmatches the environment variableADMIN_TOKEN(set via Lambda configuration) - Reads the current
receipts.jsonfrom S3 - Appends the new payment entry
- Writes the updated JSON back to S3
- Returns a 200 response with the logged entry
The receipt JSON structure is:
{
"receipts": [
{
"id": "zelle_20240115_143000",
"tenant_id": "3028",
"amount": 2500.00,
"method": "zelle",
"timestamp": "2024-01-15T14:30:00Z",
"logged_at": "2024-01-15T14:32:15Z",
"note": "Security deposit"
}
]
}
Step 4: Tenant Hub Updates
The hub's JavaScript (embedded in index.html) polls the receipts endpoint on load. The receipts section displays all logged payments with date, amount, and method. After S3 is updated and CloudFront is invalidated, the next hub page load fetches the fresh data.
Google Apps Script Integration
We enhanced the existing WarmLeadResponder.gs file to include a command handler for payment logging. When a Google Sheet receives a forwarded Zelle email (via email import), the Apps Script detects the payment pattern and calls the Lambda receipt-action endpoint:
function onPaymentEmailReceived(subject, body) {
const adminToken = PropertiesService.getScriptProperties().getProperty('ADMIN_TOKEN');
const lambdaUrl = PropertiesService.getScriptProperties().