> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cbpayapp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Protección ante rechazo del destino y reintentos seguros

> Gestiona rechazos bancarios recientes sin crear payouts duplicados

export const EnvUrls = ({lang = "en"}) => {
  const T = {
    en: {
      test: "Test",
      live: "Live",
      hint: "Same API on both environments — build against test first, then go live by swapping the base URL and the key.",
      guide: "Environments and testing",
      href: "/en/environment-testing"
    },
    es: {
      test: "Test",
      live: "Live",
      hint: "La misma API en ambos ambientes — construye primero contra test y pasa a live cambiando la URL base y la key.",
      guide: "Entorno y pruebas",
      href: "/es/environment-testing"
    },
    zh: {
      test: "Test",
      live: "Live",
      hint: "两个环境的 API 完全一致——先在 test 环境构建，再通过切换基础 URL 和密钥上线。",
      guide: "环境与测试",
      href: "/zh/environment-testing"
    }
  };
  const t = T[lang] || T.en;
  const row = (label, url, keyPattern, badgeCls) => <div className="flex flex-wrap items-center gap-2 px-3 py-2">
      <span className={"rounded px-1.5 py-0.5 text-xs font-semibold uppercase tracking-wide " + badgeCls}>
        {label}
      </span>
      <code className="text-xs">{url}</code>
      <span className="text-xs text-gray-400 dark:text-zinc-500">·</span>
      <code className="text-xs">{keyPattern}</code>
    </div>;
  return <div className="my-4 rounded-xl border border-gray-200 dark:border-zinc-700 divide-y divide-gray-200 dark:divide-zinc-700 text-sm not-prose">
      {row(t.test, "https://cryptobank.qbank.cl/platform", "pk_test_...", "bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300")}
      {row(t.live, "https://api.qbank.cl/platform", "pk_...", "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300")}
      <p className="px-3 py-2 text-xs text-gray-500 dark:text-zinc-400 m-0">
        {t.hint} <a href={t.href}>{t.guide} →</a>
      </p>
    </div>;
};

<EnvUrls lang="es" />

Consulta el [catálogo público de errores](/es/errors) para ver el contrato completo.

## Qué protege este flujo

Algunos corredores bancarios protegen un destino después de un rechazo reciente
del banco. Cuando el mismo destino normalizado fue rechazado recientemente,
CBPay detiene el siguiente create antes de crear el payout, debitar el saldo o
consumir la clave de idempotencia.

La protección se acota a la cuenta y a la combinación normalizada de
`bank_code`, `account_number` y `tax_id`. Cambiar mayúsculas o formato no la
evade.

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
  autonumber
  participant App as Tu aplicación
  participant CB as CBPay
  participant Bank as Corredor bancario
  App->>CB: POST payout (clave K1)
  CB->>CB: Revisar destino normalizado
  alt Rechazo reciente encontrado
    CB-->>App: 422 destination_recently_rejected
    Note over CB: Sin payout, débito ni consumo de clave
  else Sin rechazo reciente
    CB->>Bank: Despachar payout
    Bank-->>CB: Resultado final o ambiguo
    CB-->>App: Estado del payout
  end
```

## Secuencia segura

1. Si la primera respuesta es ambigua, repite la solicitud exacta con la misma
   `idempotency_key`. Devuelve el recurso original con
   `idempotency_hit: true`; nunca crea un segundo payout.
2. Si la respuesta es `destination_recently_rejected`, la clave original no se
   consumió. Revisa el rechazo bancario y decide si corresponde reintentar el
   destino.
3. Un reintento deliberado usa una clave de idempotencia **nueva** y
   `options.destination_retry_ack` con el ID del payout rechazado más reciente.
4. Reutilizar la clave anterior con opciones cambiadas devuelve
   `409 idempotency_conflict`; nunca modifica la operación original.
5. Un destino sin rechazo reciente, o un payout de otro corredor, sigue su
   validación y despacho normal.

El acuse es una decisión explícita del operador, no un segundo intento
automático.

## Request de reintento deliberado

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "country": "CL",
  "currency": "CLP",
  "method": "bank_transfer",
  "amount": "1000.00",
  "beneficiary": {
    "name": "Example Recipient",
    "bank_code": "012",
    "account_number": "123456789",
    "tax_id": "11111111-1"
  },
  "options": {
    "destination_retry_ack": "<prior_case_id>"
  },
  "idempotency_key": "payout-retry-0001"
}
```

El acuse debe identificar el caso rechazado más reciente de la misma cuenta y
del mismo destino normalizado. No copies un ID de caso de otra cuenta o
destino.

Para el catálogo completo de errores, consulta [Errores](/es/errors).

## Ejemplos de respuesta

### Rechazo reciente

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "error": "destination_recently_rejected",
  "message": "destination was rejected by the bank on 2026-01-15 (prior case <prior_case_id>, code <prior_code>); retry with a new idempotency_key and options.destination_retry_ack=<prior_case_id> to confirm"
}
```

El mensaje incluye la fecha UTC del rechazo, el ID del caso anterior y su
código. La respuesta pública no expone el proveedor.

### Opciones cambiadas con la clave anterior

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "error": "idempotency_conflict",
  "message": "the idempotency key was already used with a different request"
}
```

## Estados y acciones

| Respuesta o estado | Significado | Acción |
| - | - | - |
| `422 destination_recently_rejected` | Existe un rechazo reciente para el mismo destino normalizado. No se creó payout y la clave sigue disponible. | Revisa el rechazo; repite sin cambios o reintenta deliberadamente con clave nueva y acuse. |
| `409 idempotency_conflict` | La clave se reutilizó con otra solicitud u opciones. | Conserva la operación original; usa una clave nueva solo para una operación realmente nueva. |
| `processing` | El resultado todavía no es final. | Lee el payout y espera el webhook/estado final; no crees otro payout. |
| `completed` | El payout llegó a un estado final exitoso. | No requiere reintento. |
| `failed` | El payout falló y el débito siguió el camino normal de fallo. | Lee el detalle y corrige la solicitud antes de iniciar otra operación. |

## Preguntas frecuentes

<AccordionGroup>
  <Accordion title="¿El 422 crea un payout?">
    No. La guardia corre antes de insertar el payout y antes del débito. Tampoco
    consume la clave de idempotencia enviada.
  </Accordion>

  <Accordion title="¿Puedo reintentar con la misma clave después del 422?">
    Sí, si estás repitiendo exactamente la solicitud original. Un reintento
    deliberado después de revisar el rechazo debe usar clave nueva y el acuse.
  </Accordion>

  <Accordion title="¿Por qué cambiar solo las opciones devuelve 409?">
    Las opciones forman parte del payload de idempotencia. Reutilizar una clave con
    opciones distintas es otra solicitud, por eso CBPay la rechaza en vez de
    modificar silenciosamente la operación original.
  </Accordion>

  <Accordion title="¿Aplica a todos los corredores de payouts?">
    No. La protección se aplica solo donde el corredor la habilita. Los demás
    rieles y destinos sin rechazo reciente siguen su validación y despacho normal.
  </Accordion>
</AccordionGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.