Home / Property Management / Rent payment receipts from Stripe: a verified webhook, a ledger row and an email receipt
Property Management problem 4: Tenants ask 'did you get my rent?', and the ledger is updated from the Stripe dashboard by hand
Rent payment receipts from Stripe
A verified webhook, a ledger row and an email receipt
Stripe sends payment events to a webhook; the workflow checks the Stripe-Signature on the raw body (wrong secret, edited body or a replayed old event get a 400), records the payment in the rent_payments sheet and emails the tenant. Card payments get a receipt with the balance or 'paid in full'. Bank (ACH) payments are 'processing' first (the tenant is told it can take up to 4 business days), then a receipt when they clear, or a neutral 'didn't go through' note with the pay link if they fail. Partial and overpayments are flagged to the manager to decide. Payments with an unknown unit or in another currency aren't applied; the manager is told. Stripe retries never record a payment twice.
How the workflow runs
Trigger · When an event arrives
Stripe webhook (raw body)
Steps
- Verify Stripe-Signature
- Ledger row + receipt
Outcome
- Answer Stripe
- Send receipts + flags
Drawn from the node names in the workflow file inside the download (workflow.json).
The problem
Stripe's own docs on ACH timing; we found no independent figure on receipt requests, so we quote none.
- Stripe's docs: ACH Direct Debit is a delayed-notification payment method that can take up to 4 business days to confirm success or failure, which is why a 'processing' step exists. Source: Stripe docs: ACH Direct Debit (delayed notification, up to 4 business days) (Vendor docs)
- Buildium's 2025 report: 58% of renters already pay rent online or via card. Source: Buildium: 2025 Property Management Industry Report (surveys of 1,796 property managers and 1,507 renters, 2024) (Vendor industry survey)
Background reading: Rent reminder email and text templates (and when to send each one)
Who it's for
Property managers and landlords who take rent through Stripe Payment Links (for example the links in the rent reminder recipe).
Tools and monthly cost
| Tool | Official price |
|---|---|
| Resend (sends the emails; any email API that accepts an Idempotency-Key works) | Free: 3,000 emails / month, 100 / day. Pro: $20 / month for 50,000 emails pricing page |
| Google Sheets (rent roll, work orders, consent log, opt-out list, payment ledger) | Free with a Google Account pricing page |
| n8n | Community Edition: free if you self-host (you pay for the server). n8n Cloud: Starter EUR 20 / month or Pro EUR 50 / month, billed annually pricing page |
| Stripe (Payment Links; the tenant pays, you receive) | Domestic cards 2.9% + 30c per successful transaction. ACH Direct Debit 0.8% per transaction with a $5.00 cap (standard settlement), $4.00 per failed ACH payment, $15.00 per dispute. No setup or monthly fee; Payment Links are included pricing page |
Example month: Illustrative arithmetic, not a measured result. This recipe sends emails only, which stays inside Resend's free tier for a small portfolio. n8n is the only fixed cost. Stripe fees on a $1,250 rent payment: by card 2.9% + $0.30 = $36.55; by ACH Direct Debit 0.8% would be $10.00, so the $5.00 cap applies. Who pays the fee is your policy and your state's rules; check before adding a surcharge.
Setup
- Google Sheet: a rent_roll tab with columns unit, property, address, tenant_name, email, phone, rent, due_day, late_fee_note, active. Your property management software can export this; nothing here connects to Buildium or AppFolio (Buildium's Open API needs its Premium plan, and AppFolio's API is for approved partners).
- Texts need a Twilio number on an approved A2P 10DLC campaign. Set TWILIO_ACCOUNT_SID, TWILIO_BASIC_AUTH (base64 of SID:auth token) and PM_TWILIO_FROM. Point the number's Messaging webhook at /webhook/mss-pm3-sms so STOP and replies are handled in one place.
- Email: MSS_EMAIL_API (e.g. https://api.resend.com), EMAIL_API_KEY, PM_EMAIL_FROM, PM_OFFICE_EMAIL, PM_MANAGER_EMAIL. Every send carries an Idempotency-Key, so a retried run never emails twice.
- Company details: PM_COMPANY_NAME, PM_COMPANY_PHONE, PM_COMPANY_ADDRESS, COMPANY_TZ (your time zone; texting hours follow it).
- Google Sheet stand-in: MSS_SHEET_URL points at the shared sheets (rent_roll, sms_consent, sms_optouts, inbound_texts and the recipe's own sheets). In production, swap these HTTP calls for n8n's Google Sheets node.
- Remove the test-only mss-pm*-tick webhooks (or put them behind n8n header auth) and make sure MSS_TEST_CLOCK is NOT set in production.
- In Stripe (Developers > Webhooks), add an endpoint for /webhook/mss-pm4-stripe with checkout.session.completed, checkout.session.async_payment_succeeded and checkout.session.async_payment_failed, and set PM_STRIPE_WEBHOOK_SECRET to its signing secret (whsec_...).
- Your Payment Link URL must carry client_reference_id=UNIT_yyyy-MM (the rent reminder recipe adds it). Turn on ACH Direct Debit in Stripe if you want bank payments.
Download
Downloadpm-rent-payment-receipts.zip
Contains the n8n workflow, the mock data, the test runner with its last results, and the mock server we tested against. All keys in .env.example are fake.
Test result
Every workflow was imported into n8n 2.45.4 on an isolated test instance and run end to end. Twilio (signed inbound webhooks, HTTP 429), the email API (Resend-style), Stripe webhook events (signed with a mock secret, verified on the raw body) and Google Sheets were replaced by a local mock server built from the vendors' public docs. A frozen test clock checks due dates and the 8 a.m.-9 p.m. texting window. These recipes use no AI model; every decision is a written rule. Tenants, prospects, units and listings are invented. Verified with mocks; nothing was sent to a real person or account. How we test.
14/14 scenarios passed (run 2026-10-31)
| PASS | PM4-01 | Card payment in full: 200 to Stripe; 'paid' row for 12-4B / 2027-03 / $1,250.00; one receipt that says paid in full; no manager flag |
| PASS | PM4-02 | Stripe retries the same event: second delivery 200 but no second row or receipt; a different event id for an already-recorded session status is ignored too |
| PASS | PM4-03 | Wrong signing secret: 400, no row, no email |
| PASS | PM4-04 | Body changed after signing (amount edited): 400, nothing recorded |
| PASS | PM4-05 | Correct signature but timestamp 10 minutes old (replay): 400 |
| PASS | PM4-06 | Bank (ACH) payment started: 'processing' row; tenant told it can take up to 4 business days; no receipt yet |
| PASS | PM4-07 | Bank payment clears: 'paid' row for the same session; receipt sent |
| PASS | PM4-08 | Bank payment fails: 'failed' row (PM3 then treats the rent as open); tenant gets a neutral note with the pay link; office told, with the 'not lawyers' line; no fee or threat wording |
| PASS | PM4-09 | Partial payment ($500 of $1,200): receipt shows $700.00 still open; manager flagged to decide (partial-rent rules vary by state) |
| PASS | PM4-10 | Second payment overshoots ($1,300 of $1,200): manager flagged as overpayment; receipt doesn't promise a refund |
| PASS | PM4-11 | Reference doesn't match a unit: logged as 'unmatched' (counts toward nothing); manager told; nothing sent to the payer |
| PASS | PM4-12 | Non-USD payment: not applied, manager told |
| PASS | PM4-13 | Event type we don't handle (signed): 200, ignored |
| PASS | PM4-14 | Email API 429 once: retried, exactly one receipt |
Actual output from the test run
Scenario PM4-01: the receipt email after a card payment in full (invented tenant). Names and numbers are invented.
Hi Rosa, thank you. We received $1,250.00 for March 2027 rent, unit 12-4B, on October 31, 2026. March 2027 rent is paid in full. Thank you! Reference: cs_test_rosa1 Elm Street Property Management (555) 555-0160 410 Elm St, Omaha, NE
Verified with mocks
- Card payment in full: 200 to Stripe; 'paid' row for 12-4B / 2027-03 / $1,250.00; one receipt that says paid in full; no manager flag
- Stripe retries the same event: second delivery 200 but no second row or receipt; a different event id for an already-recorded session status is ignored too
- Wrong signing secret: 400, no row, no email
- Body changed after signing (amount edited): 400, nothing recorded
- Correct signature but timestamp 10 minutes old (replay): 400
- Bank (ACH) payment started: 'processing' row; tenant told it can take up to 4 business days; no receipt yet
- Bank payment clears: 'paid' row for the same session; receipt sent
- Bank payment fails: 'failed' row (PM3 then treats the rent as open); tenant gets a neutral note with the pay link; office told, with the 'not lawyers' line; no fee or threat wording
- Partial payment ($500 of $1,200): receipt shows $700.00 still open; manager flagged to decide (partial-rent rules vary by state)
- Second payment overshoots ($1,300 of $1,200): manager flagged as overpayment; receipt doesn't promise a refund
- Reference doesn't match a unit: logged as 'unmatched' (counts toward nothing); manager told; nothing sent to the payer
- Non-USD payment: not applied, manager told
- Event type we don't handle (signed): 200, ignored
- Email API 429 once: retried, exactly one receipt
- Verified with mocks; invented tenants, prospects, units and listings only.
Documented from official API docs only
Built to the vendor's documentation but not run against a live account. Check these on yours.
- Stripe webhook signing (Stripe-Signature t= / v1=, HMAC-SHA256 of timestamp.raw_body, 5-minute tolerance), Checkout Session fields (client_reference_id, payment_status, amount_total) and the async ACH events come from Stripe's public docs; tests sign events locally with a mock secret, not against a live Stripe account.
- Twilio request signing, the Messages API, 429 behavior and built-in STOP handling come from Twilio's docs. The email API follows Resend's documented request shape.
Limitations
- Only Checkout Session events from Payment Links are handled; refunds, disputes and payouts are not. Check Stripe for those.
- The receipt reflects what Stripe reported. It isn't a statement of the tenant's full account (deposits, fees and credits live in your books).
- Every outgoing message is checked against a word list first (PM_LINT in pmlib.py): eviction, court action, lawsuit, collections, credit reporting, lockouts, shut-offs, 'notice to quit/pay' and police are blocked, and so are Fair Housing red-flag phrases ('no kids', 'adults only', 'perfect for a young couple', 'English only', 'able-bodied'...). A blocked message is held and the office is told why.
- Late fees, grace periods, partial-rent rules and notice periods vary by state and city, so nothing legal is hard-coded: every day, hour and amount is a setting, and late-fee wording is only ever your own sentence from the rent roll, sent word for word. The recipes never write notices to pay or quit, never mention eviction, court or credit reporting, and never act on a lease.
- Generic stack (web form, Google Sheet, Twilio, email API, Stripe Payment Links). Not built for Buildium, AppFolio, Rent Manager or any other property management system; we make no claim these recipes plug into them.
- Not legal advice and not a compliance guarantee. This recipe sends email only; no texts.
- We're not lawyers. Nothing here is legal advice. Check HUD's Fair Housing pages (hud.gov/helping-americans/fair-housing-act-overview), the FCC texting rules (47 CFR 64.1200 on ecfr.gov) and your state's landlord-tenant law, or ask a local attorney.