> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.solvimon.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.solvimon.com/_mcp/server.

# Dunning & payment retries

This guide talks about ahat Solvimon does when an invoice is not paid: how payment attempts are retried, what dunning actions look like on the invoice, and how to drive reminders and escalation from workflows.

---

## Why this matters

A failed payment is not an exception; on a card-based subscription book it is a steady percentage of every billing run. Dunning is the process that turns those failures back into cash: retry the payment, remind the customer, and escalate when the reminders do not work. Solvimon tracks the attempts and exposes the triggers; the retry cadence itself lives with your payment service provider.

## How it works

Two separate mechanisms operate on an unpaid invoice.

**Payment retries** are attempts to charge the stored payment method again. For card payments through Adyen, this is [Adyen Auto Rescue](/integrations/paymentserviceproviders/adyen), which decides which refused or charged-back payments are worth retrying and picks the timing. Solvimon records each attempt.

**Dunning actions** are the collection steps Solvimon itself takes against an invoice: the payment requests it raises and their outcome. Each carries a status:

| Status           | Meaning                                                       |
| ---------------- | ------------------------------------------------------------- |
| `PENDING`        | The dunning action has been raised and is awaiting an outcome |
| `SUCCESSFUL`     | The payment was collected                                     |
| `FAILED`         | The attempt failed; `failure_reason` carries the detail       |
| `CANCEL_PENDING` | A cancellation has been requested and is being processed      |
| `CANCELLED`      | The dunning action was cancelled before completion            |

A dunning action references the `payment_acceptor_id` it was raised against and the `first_payment_id` of the attempt chain, so a sequence of retries on one invoice stays traceable to a single collection effort.

> **Note**
>
> Retry counts and intervals are configured at the payment service provider, not in Solvimon. Solvimon exposes the resulting attempts (`attempt`, and whether a `next_attempt_scheduled` exists) but has no retry-schedule setting of its own.

## Inspecting attempts

To see what has been tried on an invoice, read its payment attempts:

### Request

GET [https://test.api.solvimon.com/v\{version}/invoices/\{resourceId}/payment-attempts](https://test.api.solvimon.com/v\{version}/invoices/\{resourceId}/payment-attempts)

```curl
curl https://test.api.solvimon.com/v1/invoices/invo_LwFkp9AvbstgH5ZZ1Qd4/payment-attempts \
     -H "X-API-KEY: <apiKey>"
```

```python
import requests

url = "https://test.api.solvimon.com/v1/invoices/invo_LwFkp9AvbstgH5ZZ1Qd4/payment-attempts"

headers = {"X-API-KEY": "<apiKey>"}

response = requests.get(url, headers=headers)

print(response.json())
```

```javascript
const url = 'https://test.api.solvimon.com/v1/invoices/invo_LwFkp9AvbstgH5ZZ1Qd4/payment-attempts';
const options = {method: 'GET', headers: {'X-API-KEY': '<apiKey>'}};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"net/http"
	"io"
)

func main() {

	url := "https://test.api.solvimon.com/v1/invoices/invo_LwFkp9AvbstgH5ZZ1Qd4/payment-attempts"

	req, _ := http.NewRequest("GET", url, nil)

	req.Header.Add("X-API-KEY", "<apiKey>")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://test.api.solvimon.com/v1/invoices/invo_LwFkp9AvbstgH5ZZ1Qd4/payment-attempts")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Get.new(url)
request["X-API-KEY"] = '<apiKey>'

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.get("https://test.api.solvimon.com/v1/invoices/invo_LwFkp9AvbstgH5ZZ1Qd4/payment-attempts")
  .header("X-API-KEY", "<apiKey>")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('GET', 'https://test.api.solvimon.com/v1/invoices/invo_LwFkp9AvbstgH5ZZ1Qd4/payment-attempts', [
  'headers' => [
    'X-API-KEY' => '<apiKey>',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://test.api.solvimon.com/v1/invoices/invo_LwFkp9AvbstgH5ZZ1Qd4/payment-attempts");
var request = new RestRequest(Method.GET);
request.AddHeader("X-API-KEY", "<apiKey>");
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = ["X-API-KEY": "<apiKey>"]

let request = NSMutableURLRequest(url: NSURL(string: "https://test.api.solvimon.com/v1/invoices/invo_LwFkp9AvbstgH5ZZ1Qd4/payment-attempts")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

### Response (200)

```json
{
  "data": []
}
```

Each attempt carries the `attempt` number, an `external_reference` to the attempt at the provider, and `next_attempt_scheduled`, which tells you whether the provider intends to try again. When `next_attempt_scheduled` is `false` and the result is still unpaid, the automated retry chain is finished and the invoice needs human collection.

Payment results use a fixed set of values: `AUTHORIZED`, `PENDING`, `REFUSED`, `CANCELLED`, `ERROR`, `REFUNDED`, and `CHARGED_BACK`. `result_details` carries the provider's reason, which is what you surface to the customer or to your collections team.

To see the payments themselves rather than the attempt chain, use `GET /v{version}/invoices/{resourceId}/payments`.

## Stopping dunning on an invoice

When an invoice should no longer be chased, for example because it is disputed or is being settled by bank transfer, cancel its outstanding dunning actions:

### Request

POST [https://test.api.solvimon.com/v\{version}/invoices/\{resourceId}/cancel-dunning-actions](https://test.api.solvimon.com/v\{version}/invoices/\{resourceId}/cancel-dunning-actions)

```curl
curl -X POST https://test.api.solvimon.com/v1/invoices/invo_LwFkp9AvbstgH5ZZ1Qd4/cancel-dunning-actions \
     -H "X-API-KEY: <apiKey>"
```

```python
import requests

url = "https://test.api.solvimon.com/v1/invoices/invo_LwFkp9AvbstgH5ZZ1Qd4/cancel-dunning-actions"

headers = {"X-API-KEY": "<apiKey>"}

response = requests.post(url, headers=headers)

print(response.json())
```

```javascript
const url = 'https://test.api.solvimon.com/v1/invoices/invo_LwFkp9AvbstgH5ZZ1Qd4/cancel-dunning-actions';
const options = {method: 'POST', headers: {'X-API-KEY': '<apiKey>'}};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"net/http"
	"io"
)

func main() {

	url := "https://test.api.solvimon.com/v1/invoices/invo_LwFkp9AvbstgH5ZZ1Qd4/cancel-dunning-actions"

	req, _ := http.NewRequest("POST", url, nil)

	req.Header.Add("X-API-KEY", "<apiKey>")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://test.api.solvimon.com/v1/invoices/invo_LwFkp9AvbstgH5ZZ1Qd4/cancel-dunning-actions")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["X-API-KEY"] = '<apiKey>'

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://test.api.solvimon.com/v1/invoices/invo_LwFkp9AvbstgH5ZZ1Qd4/cancel-dunning-actions")
  .header("X-API-KEY", "<apiKey>")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://test.api.solvimon.com/v1/invoices/invo_LwFkp9AvbstgH5ZZ1Qd4/cancel-dunning-actions', [
  'headers' => [
    'X-API-KEY' => '<apiKey>',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://test.api.solvimon.com/v1/invoices/invo_LwFkp9AvbstgH5ZZ1Qd4/cancel-dunning-actions");
var request = new RestRequest(Method.POST);
request.AddHeader("X-API-KEY", "<apiKey>");
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = ["X-API-KEY": "<apiKey>"]

let request = NSMutableURLRequest(url: NSURL(string: "https://test.api.solvimon.com/v1/invoices/invo_LwFkp9AvbstgH5ZZ1Qd4/cancel-dunning-actions")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

### Response (200)

```json
{
  "object_type": "INVOICE",
  "id": "invo_LwFkp9AvbstgH5ZZ1Qd4",
  "invoice_number": "INV-2026-00042",
  "customer_id": "cust_AbD3DqausjOYiMNDZY11F",
  "status": "FINAL",
  "type": "STANDARD",
  "created_at": "2026-07-02T10:00:00Z",
  "updated_at": "2026-07-10T08:00:00Z",
  "billing_currency": "EUR",
  "payment_status": "UNPAID"
}
```

Actions move to `CANCEL_PENDING` and then `CANCELLED`. Cancelling dunning does not change the invoice's `payment_status`; the invoice stays unpaid until a payment is recorded or it is marked paid.

Cancelling a subscription can also cancel its dunning actions as part of the cancellation. See [Subscription lifecycle](/platform-guides/customers-and-billing/subscriptions/subscription-lifecycle).

A pending auto rescue is also cancelled automatically when the invoice is settled another way. If a payment was refused for insufficient funds, Solvimon schedules an automatic rescue with the payment provider; if the platform then pays the invoice directly through [POST /v1/invoices/\{id}/pay](https://docs.solvimon.com/api-docs/transaction-api/invoices/post-invoices-by-resource-id-pay), that pending rescue is cancelled rather than firing later and charging the customer a second time. A second auto rescue is not started for an invoice that already has one active.

> **Note**
>
> A call to the payment provider can itself fail with a rate-limit response (`429`) during a burst of payments, separately from the payment being refused. Solvimon retries a `429` with exponential backoff and jitter instead of failing the payment action, so a rate limit does not raise an alert or mark the action `ERROR`.

## Reminders and escalation with workflows

Solvimon has no fixed dunning ladder. You build the reminder and escalation sequence with [workflows](/platform-guides/workflows), which gives you control over timing, channel, and wording per billing entity or customer segment.

The two triggers that matter:

* **Failed payment.** Fires when a payment attempt fails, so you can email the customer to update their payment method, notify an account owner, or open a task in your CRM.
* **Overdue invoice.** Fires on invoices past their due date, on the schedule you set, which is how you build the reminder ladder (for example at 3, 14, and 30 days overdue).

See [Common use cases](/platform-guides/workflows/common-use-cases) for a worked overdue-reminder sequence.

## Recurring collection with payment schedules

For instalment plans and recurring collection outside the invoice cycle, a payment schedule collects on its own cadence. A schedule has a `type` of `ONE_OFF`, `RECURRING`, or `INSTALLMENTS`, and a `status` of `ACTIVE`, `PAUSED`, `CANCELLED`, or `DONE`.

Two fields matter for collections:

* `payment_method_fallback` names the method to try when the primary method fails, so a failed card can fall back to direct debit without manual intervention.
* Dunning can be enabled or disabled per schedule, so an instalment plan can be exempted from the standard chase.

Pause a schedule with `POST /v{version}/payment-schedules/{resourceIdOrReference}/pause` while a dispute is open, and activate it again afterwards, rather than cancelling and rebuilding it.

## Edge cases

* **Chargebacks are not failed payments.** A `CHARGED_BACK` payment was collected and then reversed. It does not re-enter the retry chain; handle it as a dispute.
* **Partial payments** leave the invoice `PARTIALLY_PAID`. Dunning continues against the remaining balance.
* **Overpayments** leave the invoice `OVERPAID` rather than creating a credit automatically.
* **Manual bank transfers** arrive outside the PSP, so mark the invoice paid to stop the chase. See [Payment collections](/platform-guides/invoicing/payment-collections).

---