# Cobros programados

Programar el cobro de una factura para una fecha futura, cancelarlo y adelantar un reintento, desde la API externa /external/v1.

Un cobro programado es una orden para cargar una factura concreta en una fecha futura, con la tarjeta que el usuario ya autorizó. Es lo que sirve para «cóbrame el día 8», sin que haya una cuota recurrente detrás.

Los endpoints están bajo `/external/v1`, exigen la cabecera `X-Api-Key` y comparten un límite de 60 peticiones por minuto.

Programar solo deja la orden puesta: el cargo lo ejecuta UTILIA OS cuando llega la fecha, y todo lo que pasa después te llega por los avisos de cobro: `CHARGE_STARTED`, `CHARGE_SUCCEEDED`, `CHARGE_FAILED`, `CHARGE_REQUIRES_ACTION`, `CHARGE_RETRY_SCHEDULED` y `CHARGE_RETRIES_EXHAUSTED`.

## Plan y permisos

| Concepto | Valor |
| - | - |
| Aplicativos del plan | Finanzas y Pagos con tarjeta (Stripe) |
| Capacidad del plan | Cobro automático (`finance.direct_debit`) |

Sin un aplicativo, la respuesta es `403` con `errorCode: 'MODULE_NOT_ENABLED'` y su `moduleKey`. Sin la capacidad, `403` con `errorCode: 'CAPABILITY_NOT_ENABLED'` y su `capabilityKey`.

| Endpoint | Permiso de la API (scope) |
| - | - |
| `GET /invoices/:id/scheduled-charges` | `billing:scheduled-charges:read` |
| `POST /invoices/:id/scheduled-charges` | `billing:scheduled-charges:write` |
| `DELETE /scheduled-charges/:id` | `billing:scheduled-charges:write` |
| `POST /scheduled-charges/:id/retry-now` | `billing:scheduled-charges:write` |

## Programar un cobro

**`POST /api/external/v1/invoices/:id/scheduled-charges`**

Programa el cargo de una factura emitida para una fecha futura, contra una autorización de cobro vigente del usuario.

Parámetro de ruta: `id`.

### Cuerpo

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `userId` | `string` | Sí | Identificador en tu aplicación (externalId) del usuario al que pertenece la factura. |
| `scheduledAt` | `string` | Sí | Momento de ejecución, en formato ISO 8601. |
| `paymentAuthorizationId` | `string` | Sí | Autorización de cobro que respalda el cargo. Tiene que ser de alcance SCHEDULED_INVOICE. |
| `paymentMethodId` | `string` | No | Tarjeta concreta. Tiene que ser la que respalda la autorización. |
| `idempotencyKey` | `string` | No | Clave de idempotencia, válida 24 horas. |

> **Atención: Hace falta una autorización de alcance SCHEDULED_INVOICE**
>
> Una autorización `RECURRING` cubre las cuotas de una suscripción, no un cobro puntual de una factura suelta. Si envías una, la respuesta es `409 PAYMENT_AUTHORIZATION_NOT_ACTIVE` con un mensaje que nombra el alcance que hace falta. Una aplicación que cobre cuotas **y** programe cobros sueltos necesita dos autorizaciones del mismo usuario.

A diferencia del panel, la vía externa no admite firmar la autorización en el mismo cuerpo que programa el cobro: así la prueba legal solo viaja en llamadas de consentimiento y es fácil de auditar. Recógela antes con [`POST /payment-authorizations`](https://os.utilia.ai/docs/developers/api/payment-authorizations).

### Respuesta (201)

```json
{
  "id": "cksch1234567890",
  "invoiceId": "ckinv1234567890",
  "invoiceNumber": "202600042",
  "externalUserId": "user_01HXYZ",
  "status": "PENDING",
  "scheduledAt": "2026-10-08T08:00:00.000Z",
  "amountCents": 5345,
  "currency": "eur",
  "paymentAuthorizationId": "ckauth1234567890",
  "attemptCount": 0,
  "maxAttempts": 3,
  "nextRetryAt": null,
  "failureCode": null,
  "failureMessage": null,
  "cancelReason": null,
  "cancellableUntil": "2026-10-07T08:00:00.000Z",
  "createdAt": "2026-10-01T09:14:00.000Z"
}
```

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `status` | `enum` | Sí | PENDING, RUNNING, AUTH_REQUIRED, RETRY_SCHEDULED, COMPLETED, FAILED_PERMANENTLY o CANCELLED. |
| `cancellableUntil` | `string` | Sí | Momento hasta el que se puede cancelar. Pasado ese punto, la orden entra en ejecución. |
| `failureCode` | `string` | No | Código del último rechazo. En los cobros programados viaja nulo: el fallo se guarda completo y se resume en texto en failureMessage. |
| `failureMessage` | `string` | No | Explicación legible del último rechazo. |

Produce el aviso `CHARGE_SCHEDULED`.

### Errores

| Situación | HTTP | `errorCode` |
| - | - | - |
| La factura es de otra aplicación o de otro usuario | 404 | `SCHEDULED_CHARGE_NOT_ACCESSIBLE` |
| La factura no tiene cliente asociado | 409 | `SCHEDULED_CHARGE_NOT_ACCESSIBLE` |
| La autorización indicada no es de ese usuario | 404 | `PAYMENT_AUTHORIZATION_NOT_FOUND` |
| La autorización no está vigente, o su alcance no sirve | 409 | `PAYMENT_AUTHORIZATION_NOT_ACTIVE` |
| La tarjeta indicada no es la que respalda la autorización | 404 | `PAYMENT_METHOD_NOT_FOUND` |
| El usuario indicado no existe en la aplicación | 404 | `EXTERNAL_USER_NOT_FOUND` |

## Listar los cobros programados de una factura

**`GET /api/external/v1/invoices/:id/scheduled-charges`**

Cobros programados de una factura, con sus intentos y el motivo del último rechazo.

Parámetro de ruta: `id`.

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `userId` | `string` | Sí | Identificador del usuario en tu aplicación (externalId). |
| `status` | `enum` | No | Filtra por estado. |
| `page` | `integer` | No | Página, por defecto 1. |
| `limit` | `integer` | No | Elementos por página, por defecto 20. |

La respuesta tiene la forma `{ data, total, page, limit, totalPages }`.

## Cancelar un cobro programado

**`DELETE /api/external/v1/scheduled-charges/:id`**

Cancela una orden que todavía no ha entrado en ejecución.

Parámetro de ruta: `id`.

Los dos parámetros van en la cadena de consulta:

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `userId` | `string` | Sí | Identificador del usuario en tu aplicación (externalId). |
| `cancelReason` | `string` | No | Motivo de la cancelación, para la auditoría. |

Devuelve el cobro programado ya cancelado y produce el aviso `CHARGE_CANCELLED`. Si la orden ya está en ejecución o ha terminado, responde `409 SCHEDULED_CHARGE_NOT_CANCELLABLE`.

## Adelantar un reintento

**`POST /api/external/v1/scheduled-charges/:id/retry-now`**

Adelanta el siguiente intento de un cobro que quedó a la espera de reintento.

Parámetro de ruta: `id`.

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `userId` | `string` | Sí | Identificador del usuario en tu aplicación (externalId). |

Devuelve un acuse de recibo, no el resultado del cobro.

```json
{
  "accepted": true,
  "scheduledChargeId": "cksch1234567890",
  "status": "PENDING",
  "scheduledAt": "2026-10-05T10:30:00.000Z"
}
```

Produce el aviso `CHARGE_RETRY_SCHEDULED`. Solo se admiten **cinco reintentos manuales por factura cada 24 horas**; a partir del sexto, la respuesta es `409` (no `429`) con `errorCode: 'SCHEDULED_CHARGE_RETRY_TOO_SOON'`. Los reintentos que UTILIA OS programa por su cuenta no cuentan para ese límite. Agotar el cupo no cancela el cobro: significa «hoy ya no», y el cobro programado se ejecutará igualmente en el siguiente reintento automático. Reintentar sin parar no lo acelera.

## Estados, reintentos y agotamiento

Un cobro programado se ejecuta cuando llega su fecha y, según responda el
banco, termina cobrado, queda a la espera de un reintento o se da por
perdido. Solo se puede cancelar mientras está activo, reintentando o
pendiente de autenticación.

**Estados de un cobro programado**

1. **Activa** (`PENDING`). Programado, esperando a su fecha.
   - **Cancelada** (`CANCELLED`, estado final). Se llega desde Activa cuando: La cancelas con DELETE. Aviso CHARGE_CANCELLED.
2. **Ejecutando** (`RUNNING`). Se llega desde Activa cuando: Llega la fecha y UTILIA OS lanza el cargo. El cargo está en curso.
   - **Requiere 3DS** (`AUTH_REQUIRED`). Se llega desde Ejecutando cuando: El banco pide autenticación reforzada. Aviso CHARGE_REQUIRES_ACTION; el cargo no se ha producido.
   - **Reintentando** (`RETRY_SCHEDULED`). Se llega desde Ejecutando cuando: El banco rechaza el cargo y quedan intentos. Aviso CHARGE_RETRY_SCHEDULED con los intentos que quedan.
     - **Agotada** (`FAILED_PERMANENTLY`, estado final). Se llega desde Reintentando cuando: Se agotan los intentos. Aviso CHARGE_RETRIES_EXHAUSTED.
3. **Exitosa** (`COMPLETED`, estado final). Se llega desde Ejecutando cuando: El banco acepta el cargo. Aviso CHARGE_SUCCEEDED.

Otras transiciones:

- Ejecutando → Agotada: El banco rechaza el cargo de forma definitiva: tarjeta perdida, robada o fraude.
- Reintentando → Ejecutando: Llega la fecha del reintento automático.
- Reintentando → Activa: Adelantas el reintento con retry-now.
- Reintentando → Cancelada: La cancelas con DELETE.
- Requiere 3DS → Cancelada: La cancelas con DELETE.

_Los rótulos son los del panel de Finanzas; el valor de status es el que devuelve la API._

UTILIA OS reintenta los cobros rechazados hasta agotar los intentos configurados. Cada reintento programado produce `CHARGE_RETRY_SCHEDULED` con cuántos quedan; cuando no queda ninguno, produce `CHARGE_RETRIES_EXHAUSTED` con el último código de rechazo y si la suscripción asociada ha quedado pausada.

`CHARGE_REQUIRES_ACTION` significa que el banco pide autenticación reforzada y el cargo no se ha producido. En los cobros programados, la dirección del enlace público **no** viaja en el aviso: llega `publicLinkAvailable: true` y tu aplicación pide la dirección con `POST /external/v1/invoices/:id/public-link`. De un enlace emitido solo se guarda su huella, así que no se puede reconstruir.

## Con el SDK

### JavaScript / TypeScript

```typescript
import { UtiliaSDK } from '@utilia-os/sdk-js';

const sdk = new UtiliaSDK({
  baseURL: 'https://os.utilia.ai/api',
  apiKey: process.env.UTILIA_API_KEY!,
});

const cobro = await sdk.invoices.scheduledCharges.schedule('ckinv1234567890', {
  userId: 'user_01HXYZ',
  scheduledAt: '2026-10-08T08:00:00.000Z',
  paymentAuthorizationId: 'ckauth1234567890',
  idempotencyKey: 'cobro_ckinv1234567890_08oct',
});

await sdk.invoices.scheduledCharges.cancel(cobro.id, {
  userId: 'user_01HXYZ',
  cancelReason: 'El cliente pidió aplazarlo.',
});
```

### Python

```python
import os
from utilia_sdk import UtiliaSDK
from utilia_sdk.models import (
    CancelExternalScheduledChargeInput,
    CreateExternalScheduledChargeInput,
)

sdk = UtiliaSDK(
    base_url="https://os.utilia.ai/api",
    api_key=os.environ["UTILIA_API_KEY"],
)

cobro = await sdk.invoices.scheduled_charges.schedule(
    "ckinv1234567890",
    CreateExternalScheduledChargeInput(
        user_id="user_01HXYZ",
        scheduled_at="2026-10-08T08:00:00.000Z",
        payment_authorization_id="ckauth1234567890",
        idempotency_key="cobro_ckinv1234567890_08oct",
    ),
)

await sdk.invoices.scheduled_charges.cancel(
    cobro.id,
    CancelExternalScheduledChargeInput(
        user_id="user_01HXYZ",
        cancel_reason="El cliente pidió aplazarlo.",
    ),
)
```

No hay operación para cambiar la fecha: mover una fecha que el usuario ya conoce es un encargo distinto, así que se cancela la orden y se crea otra.

## Siguiente paso

- [Autorizaciones de cobro](https://os.utilia.ai/docs/developers/api/payment-authorizations): Recoger la autorización de alcance SCHEDULED_INVOICE.
- [Acciones sobre una factura](https://os.utilia.ai/docs/developers/api/invoice-actions): Cobrar ahora mismo o pedir el enlace público.
- [Suscripciones](https://os.utilia.ai/docs/developers/api/subscriptions): Cuotas que se generan y cobran solas cada periodo.
- [Contenido de los avisos](https://os.utilia.ai/docs/developers/webhooks/events): Los ocho avisos de cobro, campo a campo.
