How to match a membership list to payment records
By the Analistable team · Updated · 6 min read
Join payments to the membership list on member ID or email. Members with no successful payment in the period are unpaid; payments that match no member are unknown payers; and failed or cancelled payment statuses give you a list of failed payments to follow up. Renewal dates turn the same join into an upcoming-renewals list.
Part of our guide: How to reconcile data in Excel
Status per member
Paid this year =SUMIFS(Payments[Amount], Payments[Member ID], [@[Member ID]],
Payments[Status], "paid", Payments[Date], ">=" & YearStart)
Failed =COUNTIFS(Payments[Member ID], [@[Member ID]], Payments[Status], "failed", Payments[Date], ">=" & YearStart)
Status =IF([@[Paid this year]] >= [@Fee], "Paid", IF([@Failed] > 0, "Payment failed", "Unpaid"))| Member | Fee | Paid this year | Failed attempts | Status |
|---|---|---|---|---|
| M-0012 | 60 | 60 | 0 | Paid |
| M-0031 | 60 | 0 | 2 | Payment failed |
| M-0045 | 60 | 30 | 0 | Part paid (instalment) |
| M-0052 | 60 | 0 | 0 | Unpaid |
Payments that match no member
On the payments sheet, =COUNTIFS(Members[Member ID], [@[Member ID]])=0 lists payments from people not on the list — new members not yet added, or members paying under a different email. Match those on name and amount before contacting anyone.
Renewals due
=FILTER(Members[[Member ID]:[Email]], (Members[Renewal date] >= TODAY()) * (Members[Renewal date] <= TODAY() + 30), "None due")The pattern behind all of these — records on one list with no match on the other — is explained in finding matches between two lists.
Worked reconciliation
| Group | Count | Action |
|---|---|---|
| Members fully paid | 412 | None |
| Members part paid | 38 | Check instalment plan |
| Members with failed payments | 17 | Ask to update card or bank details |
| Members with no payment | 23 | Renewal reminder |
| Payments with no matching member | 6 | Match by name, then add to list |
Every row of the payments export should land in exactly one of these groups. If the counts don't add up to the number of payments, some payments matched two members — usually a shared family email.
Keeping it clean
- Use a member ID in payment references (direct debit reference, payment link metadata), not just the email.
- Record lapsed members with an end date rather than deleting them, so a returning payment still finds a match.
- Run the check before renewal reminders go out, so paid members don't get chased.
Matching when there is no member ID
Payment providers often only know the payer's email. Normalise it on both sides before matching, and keep a second-choice match on surname and amount:
Email key =LOWER(TRIM([@Email]))
Member ID =XLOOKUP([@[Email key]], Members[Email key], Members[Member ID],
XLOOKUP([@Surname] & "|" & [@Amount], Members[Surname] & "|" & Members[Fee], Members[Member ID], "No match"))The fallback on surname and fee is weaker — two members called Smith on the same fee will collide — so mark those matches as Probable and check them by hand. Excel 2019 has no XLOOKUP; use IFERROR(INDEX(…, MATCH(…)), …) instead. In Google Sheets, XLOOKUP works as written with ranges.
Second example: instalments and a failed retry
M-0045 pays £60 in two instalments of £30. M-0031's direct debit failed twice, then succeeded on a retry. The payments export has one row per attempt:
| Member | Date | Amount | Status |
|---|---|---|---|
| M-0045 | 2026-01-15 | 30 | paid |
| M-0045 | 2026-07-15 | 30 | pending |
| M-0031 | 2026-01-15 | 60 | failed |
| M-0031 | 2026-01-22 | 60 | failed |
| M-0031 | 2026-02-01 | 60 | paid |
With the status formula above, M-0031 now shows Paid (£60 paid, even though there were 2 failed attempts), because paid is checked first. M-0045 shows £30 paid: the July instalment is still pending, so it is a timing item rather than an arrear. Count pending payments separately so they aren't chased.
The same check in SQL
SELECT m.member_id,
m.fee,
COALESCE(SUM(CASE WHEN p.status = 'paid' THEN p.amount END), 0) AS paid,
COUNT(CASE WHEN p.status = 'failed' THEN 1 END) AS failed_attempts,
COALESCE(SUM(CASE WHEN p.status = 'pending' THEN p.amount END), 0) AS pending
FROM members m
LEFT JOIN payments p
ON p.member_id = m.member_id AND p.date >= DATE '2026-01-01'
GROUP BY m.member_id, m.fee
ORDER BY m.member_id;The left join keeps members with no payment rows at all, which is exactly the Unpaid group. For more on left and anti joins, see joining two spreadsheets on a common column.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Paid members listed as unpaid | Payment status spelt differently (Paid, paid, succeeded) | Map every provider status to paid, failed or pending |
| Members counted twice | Two payment rows for one renewal (retry after failure) | Count members, not payment rows |
| Large group of unknown payers | Payments from a second provider without member IDs | Stack providers and match on email |
| Family members all unpaid but one | One payment covers the household | Add a household ID and compare payments per household |
| Renewals list includes lapsed members | No end date recorded | Filter out members with an end date |
Checking the totals
Two totals prove the reconciliation: paid amounts across all members plus payments with no matching member should equal successful payments in the provider export, and the number of members in all status groups should equal the number of members on the list. Do the membership check first, then pass the paid total to whoever reconciles the provider's payouts with the bank.
Doing it in Google Sheets
Membership lists often live in Google Sheets fed by a form. Keep the form responses on their own tab and never sort them; build the member list on a second tab that refers to them, and the payments export on a third. SUMIFS, COUNTIFS and FILTER all work as above with ranges. If payments come from a provider that can send data to Sheets automatically, still keep a monthly snapshot so you can see what changed.
Common mistakes
- Chasing a member whose payment is only pending.
- Matching on name alone when the payer is a partner or parent.
- Treating a refunded payment as paid because the original row still says paid.
- Deleting lapsed members, so a returning payment can no longer be matched.
Getting the exports
Export the member list with member ID, name, email, membership type, fee, join date, renewal date and end date. From each payment provider (card processor, direct debit provider, bank), export transactions for the membership year with date, amount, payer email or reference, and status. Status names vary by provider — one may say succeeded where another says paid or confirmed — so map them to three values (paid, failed, pending) before matching.
If members can pay in several ways, stack the provider exports into one Payments table with a Source column first. That way a member who failed by direct debit and then paid by card shows as Paid, not as Payment failed.
Repeating it monthly
- Export the member list and every provider's payments for the membership year to date.
- Stack payments, map statuses and refresh the status formulas.
- Work through the groups in order: unknown payers first (they may resolve unpaid members), then failed payments, then unpaid members.
- Send renewal reminders only to members still Unpaid after that.
- Save the month's workbook so you can see who moved between groups.
Frequently asked questions
- How do I find members who haven't paid?
- Sum each member's successful payments for the period with SUMIFS and flag those below the fee.
- How do I list failed direct debits?
- Count payments with a failed status per member, or filter the payments export by status and join it to the member list.
- What if members pay with a different email?
- Match on member ID where possible; otherwise review unmatched payments by name and amount.
- How do I handle family memberships?
- Give the membership one ID and list each family member under it, so payments match the membership rather than individual emails.