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

  1. Receive bank-results webhook event
  2. Filter for dishonoured payments in the event payload
  3. GET /payments/{id}: fetch full payment details
  4. Classify the dishonour.type against the Dishonour Codes reference
  5. Retryable failures: POST /payments to create a new payment on a future date
  6. Hard failures: mark the payer's source as invalid and surface a re-authorisation flow

Step 1: Receive the bank-results webhook event

The 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.

⚠️

One bank-results event can contain results for many payments. Always iterate data.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.1

Step 4: Classify the dishonour type

Check the dishonour.type against the Dishonour Codes reference. Every code falls into one of two categories:

CategoryExamplesAction
Soft failure: transient, worth retryinginsufficient-funds, payment-stopped-by-customerCreate a new payment 3–5 business days out
Hard failure: permanent, do not retryaccount-closed, invalid-account, deceasedMark source as invalid; require customer to re-authorise
⚠️

Never retry against a hard failure code. Retrying account-closed or deceased wastes 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

  1. Update your database to flag the payer's payment source as invalid
  2. Do not schedule any further payments against this source
  3. Surface a re-authorisation flow to the customer (e.g. send an email with a link to update their payment details)
  4. Once the customer provides new details, follow the Vault a Payment Source recipe to store their new source before billing again

Key caveats

  • bank-results is a batch event. It covers all payments for the day; parse the full data.payments array, 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 payment description or payer firstName when creating the payment. See Test and Live Mode for details.

What to do next

📘

Got feedback on this feature?

Send an email to [email protected] or book in a chat with our API team.


Did this page help you?