Building a Multi-Channel Payment Logging System: Email Forwarding to Tenant Portal with Lambda and Google Apps Script
This post details the implementation of a domain-isolated payment tracking system that allows property managers to forward Zelle payment confirmations directly into an automated logging pipeline, eliminating manual accounting entry while maintaining strict domain separation between business entities.
The Challenge
A property management operation needed to:
- Provision tenant portal credentials after security deposit payments
- Send those credentials through a verified, domain-appropriate email channel
- Create a frictionless way to log incoming Zelle payments without manual data entry
- Maintain complete domain isolation (no cross-contamination between
queenofsandiego.comanddangerouscentaur.com)
The existing system had sent credential emails through queenofsandiego.com, which was architecturally incorrect for a separate property management entity.
Technical Architecture
Phase 1: Domain-Isolated Credential Distribution
File Modified: /Users/cb/Documents/repos/sites/dangerouscentaur/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/index.html
Generated fresh temporary passwords for tenants and embedded them in the hub's user credentials table. The portal at https://3028fiftyfirststreet.92105.dangerouscentaur.com/ now contains hashed credentials ready for distribution.
SES Domain Setup: Rather than using a shared domain, established dangerouscentaur.com as the authoritative sender through AWS SES. This required:
- Domain verification in SES console
- DKIM token configuration via Route53 (no manual DNS changes needed with automatic DKIM enablement)
- Creation of a dedicated ImprovMX alias at
payments@dangerouscentaur.comfor inbound payment notifications
Credentials were sent via the AWS SES API using the verified dangerouscentaur.com domain, ensuring deliverability and proper authentication.
Phase 2: Email-to-Lambda Payment Logging Pipeline
New File Created: /Users/cb/Documents/repos/sites/dangerouscentaur/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/scripts/lambda-email-parser/lambda_function.py
Implemented a Google Apps Script-triggered email parser that forwards Zelle confirmations to a dedicated Lambda function. The architecture:
User Bank → Forward Email → payments@dangerouscentaur.com
→ ImprovMX → Google Apps Script trigger
→ Parse email body → Call Lambda admin endpoint
→ Log payment to receipts.json in S3
The email parser Lambda inspects incoming messages for Zelle confirmation patterns (amount, sender, timestamp) and extracts structured data without manual intervention.
Phase 3: Receipt Action Lambda Enhancement
File Modified: /Users/cb/Documents/repos/sites/dangerouscentaur/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/scripts/lambda-receipt-action/lambda_function.py
Extended the existing receipt-action Lambda with a new log_payment admin-only action. Key implementation details:
- Admin Token Authentication: Requires
X-Admin-Tokenheader matching environment variableADMIN_TOKEN(stored in Lambda environment, never in code) - Atomic S3 Operations: Reads
receipts.jsonfrom the tenant hub bucket, appends payment entry with timestamp and source (e.g., "zelle_forward"), writes back with version consistency - Idempotency: Checks for duplicate entries by Zelle confirmation ID to prevent double-logging from retry attempts
The Lambda endpoint accepts POST requests with this structure:
POST /log_payment
X-Admin-Token: [token]
Content-Type: application/json
{
"tenant_id": "3028-unit-1",
"amount": "1500.00",
"method": "zelle",
"confirmation_id": "zelle_20240415_abc123",
"source": "email_forward"
}
Phase 4: Google Apps Script Command Handler
Files Modified: /Users/cb/Documents/repos/sites/queenofsandiego.com/WarmLeadResponder.gs
Wired the GAS script to:
- Monitor incoming emails to
payments@dangerouscentaur.com(forwarded via ImprovMX webhook) - Detect Zelle confirmation patterns using regex:
/(Zelle|transfer|confirmation|ID:?\s*\w+)/i - Extract tenant identifier from forwarded message headers
- Call the Lambda
log_paymentendpoint with the admin token fromrepos.env - Log success/failure to a separate sheet for audit trail
The GAS execution uses UrlFetchApp.fetch() with proper error handling for network timeouts and Lambda throttling (exponential backoff retry).
Infrastructure & Deployment
S3 Buckets:
- Portal content hosted in the main dangerouscentaur CloudFront-backed bucket
receipts.jsonstored in same bucket under/data/receipts.json
CloudFront Distribution: After updating index.html` with fresh credentials, invalidated the entire distribution cache via:
aws cloudfront create-invalidation \
--distribution-id [DIST_ID] \
--paths "/*"
Lambda Functions Deployed:
lambda-receipt-action– Main receipt logging handler with adminlog_paymentactionlambda-email-parser(optional) – Future enhancement for automated email body parsing
Both Lambdas have Function URLs enabled (no API Gateway overhead) with authentication set to AWS_IAM for the parser, and token-based for receipt-action.
Key Design Decisions
Why ImprovMX instead of SES rule sets? ImprovMX provides a clean forwarding layer that decouples email receipt from processing logic. It's simpler to toggle on/off and doesn't require SES receipt rule configuration overhead.
Why admin token in environment variable? Prevents credential sprawl in code repositories. The token rotates independently of Lambda deployments, and GAS can securely read it from repos.env (which isn't committed to version control).
Why receipts.json in S3 instead of DynamoDB? Keeps the audit trail in the same storage layer as other tenant hub data, simplifying backup and compliance procedures.