Most club payment problems don't start with a member refusing to pay. They start three layers down, in the integration between your payment processor and whatever system is supposed to tell your treasurer that a renewal failed. The money's gone, the member thinks they're still active, and nobody notices until the quarterly reconciliation — six weeks later.
That gap is almost always a vendor problem, not a member problem. And it's mostly preventable at the procurement stage, if you know exactly what to ask for and refuse to accept vague answers.
This is the checklist I wish more treasurers had before they signed. It's written to be pasted directly into an RFP, with actual technical artifacts — webhook payloads, endpoint lists, retry policies, failure-code mappings — that separate a vendor who's done this before from one who'll make your reconciliation a nightmare.
If you've already read the operational dunning playbook for recovering dues and preventing involuntary churn, think of this as the technical procurement companion. Dunning is the process. This is the plumbing that has to work for the process to run.
Section 1: Webhook / web API requirements (the non-negotiables)
State plainly in your RFP that webhook support is mandatory, not "available on enterprise tier." Then specify exactly what events you need and what the payload must contain.
Required webhook events:
-
payment.succeeded -
payment.failed(must fire on every attempt, not only final) -
payment.retry_scheduled -
payment.refunded -
payment.partially_paid -
subscription.past_due -
subscription.cancelled -
payment_method.expiring(fires 30 days before card expiry)
That second-to-last one — payment_method.expiring — quietly prevents more failed renewals than any retry logic ever will. A vendor that sends it understands recurring billing. A vendor that doesn't, probably doesn't.
Sample webhook payload to require
``json
{
"eventid": "evt9f2a7c41",
"eventtype": "payment.failed",
"createdat": "2026-03-14T09:22:11Z",
"idempotencykey": "ren2026Q1m4821",
"attemptnumber": 2,
"maxattempts": 4,
"nextretryat": "2026-03-17T09:22:11Z",
"member": {
"externalid": "m4821",
"email": "member@example.org",
"membershiptier": "individualannual"
},
"payment": {
"amountduecents": 12000,
"amountpaidcents": 0,
"currency": "USD",
"invoiceid": "inv55210",
"paymentmethod": {
"type": "card",
"brand": "visa",
"last4": "4242",
"expiry": "2026-02"
}
},
"failure": {
"code": "expiredcard",
"category": "hard",
"networkdeclinecode": "54",
"retryable": false,
"message": "Card has expired."
}
}
``
Three fields do the heavy lifting here. idempotencykey lets your system ignore duplicate webhook deliveries — vendors retry webhook delivery too, and without this you'll double-count or double-email. failure.category distinguishing "hard" from "soft" tells you whether retrying is worth attempting at all. And attemptnumber / nextretryat let your dashboard show a member as "in retry, attempt 2 of 4, next try Thursday" instead of just a flat "failed."
Require
idempotency_keyand document its semantics (per-event unique key) so your dedup logic is reliable.
A vendor whose payload is missing idempotency_key is a red flag you should not negotiate past.
Section 2: API endpoint checklist with sample queries
Webhooks tell you when things happen. The API is how you reconcile, backfill after an outage, and let a treasurer manually resolve something. Require all of these, and ask for sample responses in the RFP.
Keep your membership organized and engaged.
Clubyly simplifies member management, event coordination, and payment collection—effortlessly.
- Unified member database
- Automated payment tracking
- Event scheduling & reminders
No credit card required
| Endpoint | Purpose | Must support |
|---|---|---|
GET /payments | List payments by date range + status | Pagination, status filter, updated_since |
GET /payments/{id} | Single payment detail | Full failure object |
GET /subscriptions/{id} | Current subscription state | Next billing date, retry state |
POST /payments/{id}/retry | Manually trigger a retry | Treasurer-initiated |
GET /invoices?status=open | Outstanding balances | Partial-payment totals |
POST /refunds | Issue full/partial refund | Idempotency key accepted |
GET /payouts | Settlement batches to your bank | For reconciliation matching |
Sample query your RFP should ask them to demonstrate:
``
GET /payments?status=failed&updated_since=2026-03-01T00:00:00Z&limit=100
``
This single query is your outage recovery plan. If your webhook listener goes down for a day, you replay every failed payment since a timestamp and catch up. A vendor whose API can't filter by updated_since forces you to pull everything and diff it yourself — ask specifically, because many can't.
Also require a GET /payouts endpoint that maps individual payments to the lump sum that actually hit your bank account. Without it, your treasurer is reverse-engineering a $4,180 deposit back into 37 individual renewals by hand. That's the most tedious task in club finance, and it's entirely avoidable at procurement.
Section 3: A reusable retry-policy template
Don't let the vendor's default retry schedule be the final word. Ask whether retry timing is configurable, and supply your own policy. Here's a template you can hand over:
``json
{
"retrypolicy": {
"maxattempts": 4,
"schedule": [
{ "attempt": 1, "delayhours": 0 },
{ "attempt": 2, "delayhours": 72 },
{ "attempt": 3, "delayhours": 120 },
{ "attempt": 4, "delayhours": 168 }
],
"retryoncategories": ["soft"],
"skiponcategories": ["hard"],
"notifymemberonattempt": [1, 3],
"escalatetotreasurerafterattempt": 3,
"finalaction": "markpastdue"
}
}
``
The logic that matters: retry soft declines, never retry hard ones. An expired card will fail four times in a row and just annoy the member while racking up processor fees. "Insufficient funds" might clear on the 1st of the month when a paycheck lands — which is exactly why spreading retries across a full week beats hammering the card every 24 hours.
A pattern worth copying: time your retries to hit on different days of the week and ideally span a month boundary. Soft declines recover far better early-month than mid-cycle.
Section 4: Failure-code → action mapping
This is the table your integration actually runs on. Require the vendor to document their full failure-code list, then map each to an action. A workable starting map:
| Failure code | Category | Retry? | Member action | Treasurer action |
|---|---|---|---|---|
insufficient_funds | soft | Yes | "Payment didn't go through, we'll retry" | None until attempt 3 |
expired_card | hard | No | Request updated card immediately | Flag for update |
card_declined | soft | Yes | Generic retry notice | None |
donothonor | soft | Yes (once) | Ask to contact bank | Review if repeats |
lostorstolen_card | hard | No | Request new payment method | Flag, do not retry |
processing_error | soft | Yes | Silent retry | None |
authentication_required | hard | No | Send 3DS/verification link | Flag |
The mistake most clubs make is treating all declines as the same red "FAILED" state. A processingerror is a transient hiccup that'll almost certainly clear on its own — messaging the member about it makes you look sloppy. A lostorstolencard will never clear and needs the member to act today. Collapsing those two into one bucket means you either over-message everyone or miss the urgent ones. The category column is what prevents that.
Section 5: Partial payments and offline reconciliation
Two things vendors love to gloss over.
Partial payments. If a member pays $80 of a $120 renewal — common with payment-plan tiers or a corrected amount — does the system track the $40 remaining, or does it either reject the payment or incorrectly mark the invoice paid? Require explicit partial-payment handling: the invoice stays open, amountpaidcents and amountduecents both update, and a payment.partially_paid webhook fires. Ask them to demonstrate it.
Offline / manual payments. Plenty of clubs still take the occasional check or cash at an event. Your system of record needs a way to log those against the same invoice so reconciliation isn't split across two universes. Require an API endpoint or dashboard action to record an offline payment with a reference field — check number, "cash-AGM-2026," whatever makes sense.
Reconciliation file formats
Require export in at least CSV, and ask whether BAI2 is supported for clubs whose bank uses it.
``csv
payoutid,paymentid,invoiceid,memberid,amountcents,feecents,netcents,status,settleddate
po7781,pay90012,inv55210,m4821,12000,65,11935,settled,2026-03-18
po7781,pay90013,inv55211,m4830,12000,65,11935,settled,2026-03-18
po7781,pay90031,inv_55240,m4702,8000,48,7952,settled,2026-03-18
``
Note feecents and netcents as separate columns — your treasurer needs gross and net to reconcile against the bank deposit and book fees correctly.
BAI2 is the bank-reconciliation format some treasurers' banks produce. If your bank uses it, a vendor who can match against BAI2 records saves hours. If the vendor has never heard of BAI2, that's fine for a small club — just know you'll be matching CSV against your bank statement manually.
Section 6: Treasurer dashboard — the components that actually matter
A dashboard that just shows total revenue is useless for preventing churn. The whole point is surfacing failures before they become cancellations. Require these components:
-
Failed-payment queue — every payment currently in a failed or retrying state, sorted by amount and days-outstanding, with the failure reason visible inline
-
Retry-in-progress list — members currently mid-retry, showing attempt number and next retry date
-
Expiring-cards panel — cards expiring in the next 30 days (this is pure churn prevention)
-
Past-due aging — balances bucketed 0–30 / 31–60 / 61–90 days
-
Unreconciled payments — anything received but not matched to a payout
-
Partial-balance list — invoices with money still owed
-
Recovery rate tile — % of failed payments eventually recovered (your single best health metric)**
Wireframe layout, top to bottom: a thin alert strip for anything urgent (hard failures needing action today), then a three-tile row (failed count / recovery rate / expiring cards), then the failed-payment queue as the main table taking up most of the screen, with aging and unreconciled as collapsible panels below.
Here's a simple visual of the dashboard workflow to include in your RFP.
The ordering principle: the first thing the treasurer sees should be what needs action today, not a vanity revenue number. Most off-the-shelf dashboards get this exactly backwards.
Section 7: SLAs and monitoring to put in the contract
Sales will promise "99.9% uptime." Make them write specifics into the agreement:
-
Webhook delivery latency events delivered within 60 seconds of the underlying event, 99% of the time
-
Webhook retry on your endpoint being down at least 24 hours of redelivery attempts with exponential backoff
-
API uptime 99.9% monthly, measured and reported
-
Incident notification you're notified within 30 minutes of a known processing outage
-
Support response payment-processing issues answered within 4 business hours (not "next business day")
Add one you'll thank yourself for later: require a webhook replay capability — the ability to re-send events from the last N days. When your listener breaks (it will, eventually), replay is how you recover without manual reconciliation.
Section 8: Security controls to confirm
Keep this tight — you mostly want evidence, not promises:
-
PCI DSS compliance — SAQ level documented; confirm card data never touches your servers (tokenization only)
-
Webhook signature verification — every webhook signed with a shared secret you can verify, so you reject forged events
-
Scoped API keys — read-only keys for the dashboard, separate write keys for refunds
-
Audit log — who issued refunds, who triggered manual retries, timestamped
-
Data export on exit — you can extract full payment history if you leave the vendor
The webhook signature one is quietly critical. An unsigned webhook endpoint is an open door for anyone who guesses your URL to post fake payment.succeeded events and mark themselves paid.
Section 9: End-to-end test cases for the QA plan
Before you go live, run these. Most vendors offer a sandbox with test cards that trigger specific declines — require that in the RFP too.
-
Successful renewal — charge a valid test card; confirm
payment.succeededwebhook arrives within 60s and member stays active. -
Soft decline + recovery — use an "insufficient funds" test card, confirm retry schedule fires, then swap to a good card mid-retry and confirm recovery is detected and the past-due flag clears.
-
Hard decline, no retry — use an "expired card" test card; confirm it does not retry and that the expiring-card / update-payment flow triggers.
-
Duplicate webhook — manually re-send the same
eventid; confirm your system dedupes viaidempotencykeyand doesn't double-email or double-count. -
Partial payment — record an $80 payment against a $120 invoice; confirm the invoice stays open with $40 due and the partial-balance list updates.
-
Offline payment — log a check payment via API/dashboard; confirm it reconciles against the invoice.
-
Listener outage + replay — disable your webhook endpoint for 30 minutes, process a payment, re-enable, and confirm the event is redelivered or recoverable via
updated_since. -
Forged webhook — post an unsigned event; confirm it's rejected.
-
Reconciliation export — pull a CSV payout export and match every line to the bank deposit total including fees.
Test cases 4 and 7 are the ones teams skip and regret. Duplicate delivery and listener downtime aren't edge cases — they happen in normal operation, and if you haven't tested them, they'll surface as a member angrily emailing that they were charged twice.
A realistic before/after
A regional professional association, roughly 600 members on annual dues around $110–$140, was running renewals through a basic checkout product that only sent a webhook on final cancellation. Their treasurer reconciled quarterly. Each cycle, somewhere around 25–30 renewals failed quietly — and because the dunning trigger arrived weeks late, maybe a third of those members had already drifted and didn't bother re-upping. Call it 8–10 avoidable lapses per quarter, a few thousand dollars a year walking out the door, plus the treasurer losing a full weekend every quarter untangling the bank deposit by hand.
After switching to a vendor that met most of this checklist — per-attempt failure webhooks, an expiring-cards feed, a live failed-payment queue, and CSV payout exports with net/fee columns — the picture changed. Failures surfaced same-day, so dunning ran on time and recovery on soft declines climbed noticeably. The quarterly reconciliation marathon shrank to a short monthly task, because the payout export matched the bank line by line. Not a dramatic revenue jump. Just fewer members lost to a broken pipe, and a treasurer who stopped dreading the end of every quarter.
When this level of rigor is overkill
If your club runs 40 members, collects dues once a year at an in-person meeting, and takes mostly checks — you don't need BAI2 reconciliation and webhook signature verification. A simple processor and a spreadsheet is genuinely fine, and the RFP above would be absurd overhead.
This matters when you're running recurring billing at enough volume that failures slip through unnoticed — roughly a few hundred members and up, or any club where renewals are automatic and spread across the year. That's the point where silent failures stop being rare enough to catch by eye.
The real value of putting these artifacts in an RFP isn't that any single vendor will tick every box. It's that their answers tell you, before you sign, whether they've genuinely built for subscription billing or are hoping you won't ask. The vendor who reads your webhook payload spec and says "yes, and here's our sandbox" is a different animal from the one who goes quiet on idempotency_key. You find that out in procurement, or you find it out six weeks into a broken reconciliation. Pick the first one.
Ready to streamline your club operations?
Join 500+ clubs using Clubyly to save time, boost member engagement, and grow their communities.