Handle a Dishonoured Direct Debit and Retry
Detect bank-results events, classify the failure, and safely retry where appropriate.
Persona: Recurring billing platform, membership system, any merchant running regular direct debit collections.
Scenario: A scheduled direct debit payment comes back dishonoured (insufficient funds, account closed, or similar) 1–3 business days after submission. This recipe shows how to detect the failure, categorise it correctly, and retry only where it's safe and appropriate to do so.
Endpoint sequence
- Receive
bank-resultswebhook event - Filter for dishonoured payments in the event payload
GET /payments/{id}: fetch full payment details- Classify the
dishonour.typeagainst the Dishonour Codes reference - Retryable failures:
POST /paymentsto create a new payment on a future date - Hard failures: mark the payer's source as invalid and surface a re-authorisation flow
Step 1: Receive the bank-results webhook event
bank-results webhook eventThe bank-results event fires once per overnight processing run and contains results for all payments processed that day. Subscribe to this event type when creating your webhook.
Onebank-resultsevent can contain results for many payments. Always iteratedata.payments; do not assume a single event corresponds to a single payment.
{
"Id": "evt_def456",
"Type": "bank-results",
"EventDate": "2026-08-02T08:00:00.000Z",
"Metadata": {
"SuccessCount": 42,
"SuccessAmount": 210000,
"DishonourCount": 2,
"DishonourAmount": 5500
},
"Data": {
"Payments": [
{
"Id": "pmt_abc001",
"Status": "approved",
"Amount": 5000,
"Payer": { "Id": "pyr_111", "Email": "[email protected]" }
},
{
"Id": "pmt_abc002",
"Status": "dishonoured",
"Amount": 5500,
"Dishonour": {
"Type": "insufficient-funds",
"Description": "Insufficient funds in account"
},
"Payer": { "Id": "pyr_222", "Email": "[email protected]" }
}
]
}
}Step 2: Filter for dishonoured payments
In your webhook handler, iterate data.payments and isolate entries where status === "dishonoured":
const dishonoured = event.Data.Payments.filter(p => p.Status === "dishonoured");Extract id, dishonour.type, amount, and payer.id from each.
Step 3: Fetch full payment details (if needed)
If you need additional fields not present in the event payload (e.g. sourceId, description, metadata), retrieve the full payment:
GET https://api.getpinch.com.au/test/payments/pmt_abc002
Authorization: ******
pinch-version: 2020.1Step 4: Classify the dishonour type
Check the dishonour.type against the Dishonour Codes reference. Every code falls into one of two categories:
| Category | Examples | Action |
|---|---|---|
| Soft failure: transient, worth retrying | insufficient-funds, payment-stopped-by-customer | Create a new payment 3–5 business days out |
| Hard failure: permanent, do not retry | account-closed, invalid-account, deceased | Mark source as invalid; require customer to re-authorise |
Never retry against a hard failure code. Retryingaccount-closedordeceasedwastes your processing fee and may trigger compliance issues.
Step 5a: Retry a soft failure
Create a new payment for the same payer. The original dishonoured payment is retained for your audit trail; do not delete it.
POST https://api.getpinch.com.au/test/payments
Authorization: ******
pinch-version: 2020.1
Content-Type: application/json
{
"payerId": "pyr_222",
"sourceId": "src_XXXXXXXX",
"amount": 5500,
"description": "Invoice INV-456 (retry 1)",
"transactionDate": "2026-08-07",
"metadata": "{\"originalPaymentId\": \"pmt_abc002\", \"retryAttempt\": 1}"
}Use metadata to track the original payment ID and how many retries have been attempted. Pinch has no built-in retry limit; defining your own cap (e.g. 3 attempts, then escalate) is your responsibility.
Step 5b: Handle a hard failure
- Update your database to flag the payer's payment source as invalid
- Do not schedule any further payments against this source
- Surface a re-authorisation flow to the customer (e.g. send an email with a link to update their payment details)
- Once the customer provides new details, follow the Vault a Payment Source recipe to store their new source before billing again
Key caveats
bank-resultsis a batch event. It covers all payments for the day; parse the fulldata.paymentsarray, not just the first item.- The original payment record is not deleted. Its status transitions to
dishonoured. Create a new payment for any retry; this preserves the full payment history. - Dishonour timing: for Australian BECS direct debit, dishonours typically arrive 1–3 business days after the payment date. Do not expect same-day results.
- Notifying customers: Pinch does not automatically send a dishonour notification to the payer on your behalf. Add your own notification logic (email, SMS) in your webhook handler.
- Time Travel for testing: to test dishonour handling in test mode, add a dishonour code (e.g.
#insufficient-funds) to the paymentdescriptionor payerfirstNamewhen creating the payment. See Test and Live Mode for details.
What to do next
- Dishonour Codes: full list of codes and recommended actions for each
- Webhooks: setting up webhook subscriptions
- Vault a Payment Source: re-collecting payment details from a customer
- Payment Statuses: full payment lifecycle reference
Got feedback on this feature?
Send an email to [email protected] or book in a chat with our API team.
Updated about 10 hours ago
