---
title: Recovery Link API
description: Generate links to your hosted recovery page from your own backend
---

The Recovery Link API lets your backend mint a link to your hosted recovery page for a customer with a failed payment. You decide where the link goes: a banner in your app, your own email, a support reply. The customer lands on the same page Churnkey recovery emails point to, updates their payment method, and pays the outstanding invoice.

## Prerequisites

- A connected **Stripe** account. Other billing providers are not supported.
- A configured recovery page domain: a Churnkey subdomain or a custom domain, set up under **Settings → Domain**. Without one the API returns an error.

Links show no campaign offer: the customer always pays the full outstanding amount, exactly like the in-app [Failed Payment Wall](/failed-payment-recovery/failed-payment-wall). Generating a link does not stop a running recovery campaign; the campaign ends when the payment is recovered.

## Generate a link

**Endpoint**

```bash
POST https://api.churnkey.co/v1/recovery-sessions
```

**Authentication**

All requests require your App ID and Data API Key, which you can find under **Settings → Organization** in your [Churnkey Dashboard](https://app.churnkey.co/settings/organization). The key never belongs in browser code: your server talks to Churnkey, and only the resulting link reaches the customer.

```
x-ck-app: YOUR_APP_ID
x-ck-api-key: YOUR_DATA_API_KEY
Content-Type: application/json
```

Use your live key for live customers and your test key against test billing data. Churnkey reads the mode from the key prefix (`live_data_...` or `test_data_...`) and echoes it back as `mode` in the response.

**Request body**

| Parameter    | Type     | Required | Description                                                                                                                              |
| ------------ | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `customerId` | `string` | Yes      | The Stripe customer ID (`cus_...`).                                                                                                       |
| `invoiceId`  | `string` | No       | A specific open invoice (`in_...`) to target. Without it, Churnkey runs the same lookup as the Failed Payment Wall: an open invoice with a balance, whose due date has passed and falls within the last 60 days. |
| `expiresAt`  | `string` | No       | ISO 8601 timestamp for when the link stops working. Between 1 hour and 30 days from now. Defaults to 5 days.                              |

**Example request**

```bash
curl -X POST https://api.churnkey.co/v1/recovery-sessions \
  -H "x-ck-api-key: YOUR_DATA_API_KEY" \
  -H "x-ck-app: YOUR_APP_ID" \
  -H "Content-Type: application/json" \
  -d '{ "customerId": "cus_abc123" }'
```

**Example response**

```json
{
  "id": "665f1c2e8f1b2a0012345678",
  "url": "https://billing.yourcompany.com/?auth=eyJhbGciOi...",
  "expiresAt": "2026-09-01T12:00:00.000Z",
  "invoiceId": "in_1OZDeWAjZMdyHS1H",
  "mode": "live",
  "attributable": true
}
```

For most integrations you only need `url`. Hand it to the customer however fits your product; the link is already authenticated, so there is no verification step in between.

Repeated calls for the same invoice return the existing unexpired link, with its original expiry: a different `expiresAt` on a repeat call is ignored. To change the expiry, revoke the link and generate a new one. Keep `id` if you plan to revoke it later.

**Error responses**

| Status | Meaning                                                                                    |
| ------ | ------------------------------------------------------------------------------------------ |
| `400`  | Missing `customerId`, or an `expiresAt` that is not a valid date within the allowed range.   |
| `401`  | Missing or invalid `x-ck-app` / `x-ck-api-key`.                                             |
| `404`  | No outstanding failed invoice for this customer, or the named invoice was not found.        |
| `422`  | No recovery page domain configured, no connected Stripe account, the invoice has no outstanding balance, or the account recovers PaymentIntents rather than invoices, which this endpoint does not support yet. |
| `503`  | Stripe could not be reached to read the invoice. Safe to retry.                              |

## Attribution

A payment recovered through an API-generated link is counted under the **Assisted** method in your recovery analytics. See [Recovery Methods](/failed-payment-recovery/analytics/recovery-methods).

Some links cannot be attributed. When Churnkey holds no record of the failed payment, the customer can still pay (the invoice is read from Stripe), but the recovery is missing from analytics. This happens for one-off invoices not tied to a subscription (excluded from Payment Recovery by default) and for customers excluded from recovery campaigns. In that case the response carries `"attributable": false` and an explanatory `attributionNote`.

```json
{
  "id": "665f1c2e8f1b2a0012345678",
  "url": "https://billing.yourcompany.com/?auth=eyJhbGciOi...",
  "expiresAt": "2026-09-01T12:00:00.000Z",
  "invoiceId": "in_1OZDeWAjZMdyHS1H",
  "mode": "live",
  "attributable": false,
  "attributionNote": "No recovery campaign matches this invoice; a recovery through this link will not be attributed to Churnkey."
}
```

## Revoke a link

```bash
DELETE https://api.churnkey.co/v1/recovery-sessions/:id
```

Uses the same authentication headers, with the key that minted the link: a live key cannot revoke a test link or the other way round. The `id` comes from the generate response. Revoking expires the link immediately; a later generate call for the same invoice mints a fresh one. Revoking an already expired or revoked link returns `404`.

## Security notes

- The link authenticates the customer: anyone holding it can see the customer's billing details and pay the invoice. Treat it like a password reset link.
- Choose the shortest expiry that fits your flow. A link behind a button in your logged-in app can live for hours; a link sent by email may need days.
- Links are not single-use. Revoke a link if it may have leaked.
