Skip to content
Hardcount
Run it locally
Pre-1.0 · one maintainer · Stripe and cards only · no SDKs (client libraries) yet · no production users yet. Docs describe v0.9.1 (29 Oct 2026) · What’s new →

Create dunning policy

POST
/v1/dunning/policies
curl --request POST \
--url http://localhost:8080/v1/dunning/policies \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "name": "example", "enabled": false, "retry_days": [ 3, 4, 7 ], "final_subscription_action": "none", "final_invoice_action": "none" }'

A dunning policy decides how failed payments are retried and what happens when the retries run out. Every account starts with one policy per mode, “Default retries”, as the default. The first enabled policy you create in a mode takes the default from it, unless you have edited it or made it the default yourself; later ones do not (use set-default). Customers without an assigned policy use the default, so check is_default in the response.

enabled defaults to false. A disabled policy starts no dunning, so send enabled: true for the policy to take effect. A policy created disabled never takes the default from “Default retries”.

Idempotency-Key
string

Makes the call safe to retry. Send a unique value (a UUID works) and the same value on every retry. For 24 hours, a repeat with the same key, method, path and body gets the first response again, with an Idempotent-Replayed: true header, instead of running again. The error cases are in docs/api-surface.md. The usage routes do not take it: each usage event carries its own idempotency_key.

Media typeapplication/json
object
name
string
<= 255 characters
enabled

A disabled policy starts no dunning.

boolean
retry_days
required

Days to wait before each retry. The first counts from the failed payment, or, with no card on file, from the collection that found none. Each later one counts from the retry before it: [3, 4, 7] retries on day 3, 7 and 14. The number of retries is the length of the list. A day is 24 hours. retry_schedule, max_retry_attempts and grace_period_days are refused.

Array<integer>
>= 1 items <= 15 items

Example

[
3,
4,
7
]
final_subscription_action

What happens to the subscription when retries run out. pause pauses collection and sends subscription.collection_paused with triggered_by: dunning. It does nothing if collection is already paused, the subscription is canceled, or the invoice was paid first. cancel cancels the subscription and sends subscription.canceled with canceled_by: dunning. It does nothing if the subscription is already canceled. If a scheduled cancel was already due, that cancel happens instead, and its event says canceled_by: schedule.

string
default: pause
Allowed values: none pause cancel
final_invoice_action

What happens to the invoice when retries run out.

string
default: none
Allowed values: none mark_uncollectible

The new policy

Media typeapplication/json
object
id
required
string
tenant_id
string
name
required
string
enabled
required
boolean
is_default
required

Customers without an assigned policy use the default.

boolean
seeded
required

True for the “Default retries” policy the account was created with, until you edit it or make it the default. While it is true, the next enabled policy you create becomes the default instead. Set by the server: a request body that sends it is refused with 422.

boolean
retry_days
required

Days to wait before each retry; the number of retries is its length.

Array<integer>
final_subscription_action
required
string
Allowed values: none pause cancel
final_invoice_action
required
string
Allowed values: none mark_uncollectible
created_at
required
string format: date-time
updated_at
required
string format: date-time

Example

{
"id": "hc_dpol_1a2b3c4d5e6f7890",
"retry_days": [
3,
4,
7
],
"final_subscription_action": "none",
"final_invoice_action": "none"
}

invalid_request: the body is not valid JSON.

Media typeapplication/json
object
error
object
type

One of invalid_request_error, authentication_error, rate_limit_error or api_error. Every error the API answers has this shape, including an unknown path (404, code route_not_found) and a method the path does not take (405, code method_not_allowed, with an Allow header). The few exceptions are listed in docs/api-surface.md.

string
code
string
message
string
request_id
string
param

The request field the error is about. Sent only when the error names one. A request body may hold only the fields its operation lists, spelled and cased as listed: any other field, or one the server sets (such as id), is refused with 422 and param naming it. An operation with no request body lists no fields: send no body, {} or null. The Stripe and Postmark webhooks and the LiteLLM callback take their sender’s body as it comes.

string

Example

{
"error": {
"type": "invalid_request_error"
}
}

validation_error: a replaced field was sent (final_action, retry_schedule, max_retry_attempts, grace_period_days), retry_days is missing or out of bounds, or an unknown final action.

Media typeapplication/json
object
error
object
type

One of invalid_request_error, authentication_error, rate_limit_error or api_error. Every error the API answers has this shape, including an unknown path (404, code route_not_found) and a method the path does not take (405, code method_not_allowed, with an Allow header). The few exceptions are listed in docs/api-surface.md.

string
code
string
message
string
request_id
string
param

The request field the error is about. Sent only when the error names one. A request body may hold only the fields its operation lists, spelled and cased as listed: any other field, or one the server sets (such as id), is refused with 422 and param naming it. An operation with no request body lists no fields: send no body, {} or null. The Stripe and Postmark webhooks and the LiteLLM callback take their sender’s body as it comes.

string

Example

{
"error": {
"type": "invalid_request_error"
}
}