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

# Resolver cobro (Testing)

> Aprueba o rechaza un cobro de prueba como si el cliente hubiera pagado o el banco hubiera rechazado el pago.

<Warning>
  Este endpoint solo funciona con cobros de prueba (`is_test: true`), creados con una llave `sk_test_`. Si intentas resolver un cobro de producción, recibirás un error `403`.
</Warning>

Simula el resultado del pago de un cobro sin abrir el link ni pagar en el checkout. Sirve para probar cómo reacciona tu integración cuando un cobro se aprueba o se rechaza, incluidos los webhooks `payment.approved` y `payment.rejected`.

### ¿Cómo funciona?

El cobro recorre el mismo camino que un pago real:

1. **Si el cliente ya empezó a pagar** (por ejemplo, un PSE que quedó en `processing`), se resuelve ese intento.
2. **Si nadie ha intentado pagar**, OnePay crea un intento de prueba (método `Sandbox`) por el valor del cobro y lo resuelve.
3. El cobro cambia de estado y OnePay envía el webhook correspondiente:

| `action`  | Intento  | Cobro      | Webhook            |
| --------- | -------- | ---------- | ------------------ |
| `approve` | `paid`   | `approved` | `payment.approved` |
| `reject`  | `failed` | `declined` | `payment.rejected` |

El intento queda registrado y lo puedes consultar en [Intentos de pago](/client/payments/intents).

<Note>
  La respuesta trae el **intento** ya resuelto. El **cobro** cambia de estado unos segundos después, de forma asíncrona, igual que con un pago real. Confirma el resultado con el webhook o con [Ver cobro](/client/payments/detail).
</Note>

<Warning>
  **Un cobro con `phone` vuelve a `pending` después del rechazo.** Cuando un cobro se rechaza, OnePay le reenvía la solicitud de pago al cliente para que lo intente de nuevo, y el cobro regresa a `pending` sin enviar otro webhook. El webhook `payment.rejected` sí se envía. Si quieres que el cobro se quede en `declined` durante la prueba, créalo sin `phone`.
</Warning>

### Estados que se pueden resolver

Solo los cobros en `pending`, `in_progress` o `declined`. Resolver un cobro en `declined` simula que el cliente reintentó el pago: con `approve` el cobro pasa a `approved`.

<ParamField path="payment_id" type="string" required placeholder="9e02ac3f-8d1b-4f5e-9c2a-7b3d4e5f6a7b">
  ID del cobro a resolver. [Crear cobro](/client/payments/create).
</ParamField>

### Body

<ParamField body="action" type="string" required>
  Resultado que quieres simular. Valores válidos:

  * `approve`: el cliente pagó. El cobro pasa a `approved`.
  * `reject`: el banco o el procesador rechazó el pago. El cobro pasa a `declined`.
</ParamField>

<ParamField body="remarks" type="string">
  Motivo del rechazo. **Requerido cuando `action` es `reject`**.

  Acepta los mismos códigos de los [escenarios de prueba](/client/charges/scenarios), por ejemplo:

  * `INSUFFICIENT_FUNDS`: fondos insuficientes
  * `TRANSACTION_REJECTED`: rechazado por el banco
  * `RISK_CONTROL`: bloqueado por control de riesgo
  * `CARD_EXPIRED`: tarjeta expirada

  El motivo queda en `remarks` del intento y del cobro.
</ParamField>

<RequestExample>
  ```bash cURL theme={null}
  curl https://api.onepay.la/v1/payments/9e02ac3f-8d1b-4f5e-9c2a-7b3d4e5f6a7b/resolve \
    -X POST \
    -H "Authorization: Bearer sk_test_KjbE0Vobf5Lg1yNZ4HoKawDItDs3Rod9VXNBWrEYmGn06tkesPIi" \
    -H "Content-Type: application/json" \
    -d '{
      "action": "reject",
      "remarks": "INSUFFICIENT_FUNDS"
    }'
  ```

  ```javascript JavaScript theme={null}
  const paymentId = '9e02ac3f-8d1b-4f5e-9c2a-7b3d4e5f6a7b';

  const response = await fetch(`https://api.onepay.la/v1/payments/${paymentId}/resolve`, {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer sk_test_KjbE0Vobf5Lg1yNZ4HoKawDItDs3Rod9VXNBWrEYmGn06tkesPIi',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      action: 'reject',
      remarks: 'INSUFFICIENT_FUNDS'
    })
  });

  const attempt = await response.json();
  console.log(attempt.status); // "failed"
  ```

  ```python Python theme={null}
  import requests

  payment_id = "9e02ac3f-8d1b-4f5e-9c2a-7b3d4e5f6a7b"
  url = f"https://api.onepay.la/v1/payments/{payment_id}/resolve"

  response = requests.post(url, headers={
      "Authorization": "Bearer sk_test_KjbE0Vobf5Lg1yNZ4HoKawDItDs3Rod9VXNBWrEYmGn06tkesPIi",
      "Content-Type": "application/json"
  }, json={
      "action": "reject",
      "remarks": "INSUFFICIENT_FUNDS"
  })

  attempt = response.json()
  print(attempt["status"])  # "failed"
  ```

  ```php PHP theme={null}
  <?php
  $paymentId = "9e02ac3f-8d1b-4f5e-9c2a-7b3d4e5f6a7b";
  $ch = curl_init();

  curl_setopt_array($ch, [
      CURLOPT_URL => "https://api.onepay.la/v1/payments/{$paymentId}/resolve",
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_POST => true,
      CURLOPT_HTTPHEADER => [
          "Authorization: Bearer sk_test_KjbE0Vobf5Lg1yNZ4HoKawDItDs3Rod9VXNBWrEYmGn06tkesPIi",
          "Content-Type: application/json"
      ],
      CURLOPT_POSTFIELDS => json_encode([
          "action" => "reject",
          "remarks" => "INSUFFICIENT_FUNDS"
      ])
  ]);

  $response = curl_exec($ch);
  $attempt = json_decode($response, true);
  echo $attempt["status"]; // "failed"

  curl_close($ch);
  ?>
  ```
</RequestExample>

### Aprobar un cobro

```bash theme={null}
curl https://api.onepay.la/v1/payments/9e02ac3f-8d1b-4f5e-9c2a-7b3d4e5f6a7b/resolve \
  -X POST \
  -H "Authorization: Bearer sk_test_KjbE0Vobf5Lg1yNZ4HoKawDItDs3Rod9VXNBWrEYmGn06tkesPIi" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "approve"
  }'
```

### Escenario: rechazo y reintento exitoso

Para probar de punta a punta el camino de un cobro rechazado:

1. Crea un cobro de prueba sin `phone` con [Crear cobro](/client/payments/create).
2. Recházalo con `"action": "reject"` y un `remarks`. El cobro queda en `declined` y, si tienes un [webhook](/client/webhooks/create) suscrito, recibes `payment.rejected`.
3. Apruébalo con `"action": "approve"`, como si el cliente hubiera reintentado. El cobro queda en `approved` y recibes `payment.approved`.
4. Consulta los dos intentos en [Intentos de pago](/client/payments/intents).

### Respuesta

Devuelve el intento resuelto, con el mismo formato de [Intentos de pago](/client/payments/intents).

<ResponseField name="id" type="string">
  ID del intento de pago resuelto.
</ResponseField>

<ResponseField name="status" type="string">
  Estado del intento: `paid` si se aprobó, `failed` si se rechazó.
</ResponseField>

<ResponseField name="remarks" type="object | null">
  Motivo del rechazo, con `code`, `description`, `customer_description` y `retry`. Es `null` cuando se aprueba.
</ResponseField>

<ResponseField name="payment_method_type" type="string">
  `Sandbox` si OnePay creó el intento, o el método que eligió el cliente si ya había empezado a pagar.
</ResponseField>

<ResponseField name="paid_at" type="string | null">
  Fecha de pago del intento. Es `null` cuando se rechaza.
</ResponseField>

<ResponseExample>
  ```json 200 Rechazado theme={null}
  {
    "id": "9e02966f-2ddf-4ee7-a391-5b5b7653e232",
    "status": "failed",
    "currency": "COP",
    "amount": 50000,
    "amount_label": "$50.000",
    "payment_method_label": "Sandbox",
    "payment_method_type": "Sandbox",
    "payment_method_id": "9e02966f-7c1a-4b2e-8f3d-2a6b9c4d1e70",
    "network_id": null,
    "transaction_id": null,
    "remarks": {
      "code": "INSUFFICIENT_FUNDS",
      "description": "Fondos insuficientes.",
      "customer_description": "Fondos insuficientes. Asegúrate de contar con dinero suficiente en tu método de pago.",
      "retry": false
    },
    "is_test": true,
    "created_at": "2026-09-22T21:09:28.000000Z",
    "paid_at": null
  }
  ```

  ```json 200 Aprobado theme={null}
  {
    "id": "9e02966f-2ddf-4ee7-a391-5b5b7653e232",
    "status": "paid",
    "currency": "COP",
    "amount": 50000,
    "amount_label": "$50.000",
    "payment_method_label": "Sandbox",
    "payment_method_type": "Sandbox",
    "payment_method_id": "9e02966f-7c1a-4b2e-8f3d-2a6b9c4d1e70",
    "network_id": null,
    "transaction_id": "9e02966f-bbf6-403a-a5b9-0875242ec6d3",
    "remarks": null,
    "is_test": true,
    "created_at": "2026-09-22T21:09:26.000000Z",
    "paid_at": "2026-09-22T21:09:26.000000Z"
  }
  ```

  ```json 403 Cobro no es de prueba theme={null}
  {
    "message": "This endpoint is only available for test payments."
  }
  ```

  ```json 422 Estado no resoluble theme={null}
  {
    "message": "Only payments in pending, in_progress or declined status can be resolved."
  }
  ```

  ```json 422 Remarks requerido theme={null}
  {
    "message": "El campo remarks es obligatorio cuando action es reject.",
    "code": 10001,
    "code_name": "validation_error",
    "errors": {
      "remarks": ["El campo remarks es obligatorio cuando action es reject."]
    }
  }
  ```

  ```json 422 Ya pagado theme={null}
  {
    "message": "Este pago ya fue pagado.",
    "code": 15003,
    "code_name": "payment_already_paid"
  }
  ```
</ResponseExample>
