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

# Confirmar pago

> Registra el cobro de una orden abierta y la salda. Acepta varios medios de pago en un mismo cobro y también los intentos rechazados.

Cierra el ciclo del **pago diferido**: la orden nació abierta, la cocina ya trabajó, y acá llega el
cobro. La orden pasa a `COMPLETED` y arranca la facturación cuando lo aprobado llega **exacto** al
total de la orden.

<Note>
  **Un cobro con varias piezas, en una sola llamada.** Si repartís la cuenta entre efectivo y
  tarjeta, mandá las dos piezas **en la misma llamada**: las piezas aprobadas tienen que sumar
  exacto el total de la orden. Un cobro que no cuadra se rechaza entero y no se registra nada —
  consolidá tus parciales antes de mandarlos.

  Reintentar es seguro y esperado: reenviá el envío **completo** y la idempotencia por
  `transaction_id` se encarga del resto.
</Note>

## Autenticación

<ParamField header="x-api-key" type="string" required>
  Tu API key de Fire con scope `orders:write`. La key **debe ser vendor-scoped** — las keys sin
  `vendorId` se rechazan con `403`.
</ParamField>

## Path params

<ParamField path="orderId" type="string" required>
  Cualquiera de las tres referencias públicas de la orden:

  | referencia          | qué es                                  |
  | ------------------- | --------------------------------------- |
  | `orders.id`         | el UUID interno de Fire                 |
  | `order_external`    | el id que asignaste al crear la orden   |
  | `metadata.order_id` | copia del id externo dentro de la orden |

  Es el mismo conjunto que aceptan [Obtener orden](/es/api-reference/get-order) y
  [Cancelar orden](/es/api-reference/cancel-order).
</ParamField>

## Cuerpo

<ParamField body="payments" type="object[]" required>
  Los medios con los que cobraste la orden. Entre 1 y 20 piezas. Cada objeto se guarda tal cual lo
  enviaste; abajo solo están los campos que Fire lee.
</ParamField>

<ParamField body="payments[].details.transaction_status" type="string" required>
  Estado del intento. Cuentan como **cobrado**: `APPROVED`, `AUTHORIZED`, `CAPTURED`, `PAID`,
  `SUCCESS`, `SUCCEEDED` (sin distinguir mayúsculas).

  Cualquier otro valor se trata como **rechazo**, incluso si no lo reconocemos. Es deliberado:
  facturar un cobro que no ocurrió no tiene vuelta atrás, mientras que no saldar algo que sí se
  cobró se resuelve con otro envío.
</ParamField>

<ParamField body="payments[].details.transaction_id" type="string" required>
  Identificador de la transacción. **Es la clave de idempotencia**: reenviar el mismo no vuelve a
  cobrar ni a facturar. Si tu medio no lo genera, Fire usa `payments[].uid`.
</ParamField>

<ParamField body="payments[].details.total_bill" type="number" required>
  Monto de esta pieza, decimal. Debe ser mayor que cero. Si no viene, Fire cae al `payments[].total`
  del nivel superior.
</ParamField>

<ParamField body="payments[].details.currency_code" type="string" required>
  Moneda de la pieza. Todas las piezas **aprobadas** deben compartir la misma — incluidas las de
  llamadas anteriores de la misma orden. Si no viene, Fire cae al `payments[].currency_code` del
  nivel superior.
</ParamField>

<ParamField body="payments[].details.decline_reason" type="string">
  Motivo del rechazo, con un código de [Motivos de rechazo](/es/api-reference/payment-decline-reasons).
  Solo aplica cuando `transaction_status` no es aprobado.

  El código de tu adquirente (`51`, `do_not_honor`) hay que traducirlo **de tu lado** al del catálogo:
  Fire no guarda los códigos de cada proveedor. Si mandás uno que no está, se acepta y se guarda,
  pero vuelve con `resolvedTo: null` y queda fuera de tus métricas agrupadas.
</ParamField>

<ParamField body="payments[].method" type="string">
  Medio de pago (`CASH`, `CREDIT`, `DEBIT`, `PIX`…). Se usa para la facturación y las métricas.
</ParamField>

## Petición

<RequestExample>
  ```http theme={null}
  POST https://api.fire.rest/api/v1/fire/external/orders/ORD-77/confirm-payment
  x-api-key: <tu_api_key>
  Content-Type: application/json

  {
    "payments": [
      {
        "uid": "9b1c...",
        "total": "20",
        "method": "CASH",
        "currency_code": "BRL",
        "details": {
          "total_bill": 20,
          "currency_code": "BRL",
          "transaction_id": "BR-K000-POS-38-1785258648480",
          "transaction_status": "APPROVED",
          "transaction_date": { "date": "2026-07-28T17:10:48.000Z" }
        }
      },
      {
        "uid": "7d3a...",
        "total": "15.90",
        "method": "CREDIT",
        "currency_code": "BRL",
        "details": {
          "total_bill": 15.90,
          "currency_code": "BRL",
          "transaction_id": "BR-K000-POS-38-1785258648999",
          "transaction_status": "APPROVED",
          "transaction_date": { "date": "2026-07-28T17:11:02.000Z" }
        }
      }
    ]
  }
  ```
</RequestExample>

## Qué hace Fire con lo que enviás

Solo las piezas **aprobadas** suman y saldan. Las rechazadas se registran para tus métricas, no
suman y nunca bloquean el cobro.

| situación                                                                      | `outcome`   | ¿salda?                                          |
| ------------------------------------------------------------------------------ | ----------- | ------------------------------------------------ |
| la suma de las aprobadas **es igual** al total                                 | `settled`   | sí — pasa a `COMPLETED` y arranca la facturación |
| ninguna pieza aprobada, solo rechazos                                          | `declined`  | no — se registra y la orden sigue abierta        |
| ninguna pieza nueva (ya las habías enviado)                                    | `duplicate` | no — reintento idempotente                       |
| la suma de las aprobadas **no es igual** al total                              | —           | **`400`** — no se registra nada                  |
| una pieza **nueva** sobre una orden **cerrada** (saldada, cancelada o forzada) | —           | **`409`** — no se registra nada                  |

<Warning>
  **Todo o nada.** Un cobro cuyas piezas aprobadas no suman el total de la orden se rechaza entero —
  ni siquiera se guarda un rechazo que viniera en el mismo envío.

  El motivo no es contable, es operativo: aceptar plata en una orden que no salda la deja con dinero
  adentro y sin salida. Fire no procesa devoluciones y no hay forma de que nos avises que
  devolviste. Consolidar los cobros parciales te toca a vos — y sos el único que puede devolver, así
  que ese estado ya lo cargás igual.

  Cobrar **más** que el total se rechaza por otra razón: facturaría un monto que no es el que se
  cobró, y una factura no se puede desemitir. Los dos mensajes te dan los dos montos para que
  corrijas y reenvíes.

  Los rechazos son la excepción: un envío **sin** piezas aprobadas se registra y la orden sigue
  abierta. Sin plata no hay estado que resolver, y ahí viven tus métricas de rechazo.
</Warning>

## Qué pasó con cada rechazo

Cada pieza rechazada vuelve en `declines[]`, con lo que enviaste y si lo encontramos en el catálogo.

```jsonc theme={null}
"declines": [
  { "sent": "INSUFFICIENT_FUNDS", "resolvedTo": "INSUFFICIENT_FUNDS", "group": "funds", "exceedsOrderTotal": false },
  { "sent": "51",                 "resolvedTo": null,                 "group": null,    "exceedsOrderTotal": true }
]
```

`exceedsOrderTotal: true` quiere decir que el monto que intentaste cobrar **supera el total de la
orden**. No exigimos que cada pieza sea igual al total —en un cobro repartido, \$20 sobre \$35,90 es
legítimo— pero superarlo nunca lo es: casi siempre significa que se está cobrando otra orden. El
rechazo es tu aviso gratis, porque si el siguiente intento aprueba se saldaría un monto que no
corresponde.

`resolvedTo: null` quiere decir que ese código **no existe en el catálogo** — en el ejemplo, se mandó
el código crudo del adquirente en vez del de Fire. El rechazo se registró igual y el valor quedó
guardado, pero no aparece en nada agrupado por motivo.

<Warning>
  Revisá este bloque en tu primera integración. Un código mal traducido no rompe nada: el cobro
  funciona, la respuesta es `200`, y tus rechazos entran sin clasificar. Te enterarías meses después
  con el tablero de motivos vacío.
</Warning>

## Una orden cerrada no acepta nada más

Una orden recibe cobros **mientras está abierta**. Una vez cerrada está cerrada, sin importar cómo
llegó a estarlo. Una pieza que nunca vio se **rechaza** y no se registra nada — ni aprobada ni
rechazada.

El código de error te dice *por qué* está cerrada, y las dos cosas piden acciones distintas:

| código                  | la orden                                   | qué significa para vos                                 |
| ----------------------- | ------------------------------------------ | ------------------------------------------------------ |
| `ORDER_ALREADY_SETTLED` | ya se cobró por completo                   | la plata entró. Si cobraste de nuevo, hay que devolver |
| `ORDER_NOT_OPEN`        | se cerró sin cobrarse (cancelada, forzada) | esa plata no corresponde a esta orden                  |

Lo que separa un rechazo de un reintento es el `transaction_id`, no el estado de la orden:

| lo que enviás                          | qué es                                    | respuesta                   |
| -------------------------------------- | ----------------------------------------- | --------------------------- |
| un `transaction_id` que Fire ya tiene  | un reintento después de un timeout de red | `200`, `outcome: duplicate` |
| un `transaction_id` que Fire nunca vio | un reporte sobre una orden cerrada        | `409`                       |

<Warning>
  Un reintento nunca se rechaza, a propósito. Cobraste una vez y nuestra respuesta nunca te llegó;
  contestar con un error ahí empujaría al operador a pasar la tarjeta de nuevo — el doble cobro que
  estamos tratando de evitar. Mandá el mismo `transaction_id` y recibís de vuelta el resultado
  original.
</Warning>

## Cuando el cobro salda

Saldar es lo que completa la orden, así que ahí es cuando Fire emite
[`order.completed`](/es/events/order-completed) — la orden nació `OPEN` y recién ahora
terminó. La respuesta lo informa como `flowsTriggered`.

`flowsTriggered: 0` **no** es un error de cobro. Significa una de tres cosas:

| Caso                   | Por qué                                                                                                 |
| ---------------------- | ------------------------------------------------------------------------------------------------------- |
| El cobro no saldó      | La orden sigue abierta; no se completó nada.                                                            |
| `outcome: "duplicate"` | El evento ya salió con la llamada original.                                                             |
| Falló el encolado      | El pago quedó registrado igual. El incidente queda del lado de Fire para que un operador lo re-dispare. |

Esa última fila es deliberada. Si Fire respondiera con error porque no pudo encolar un
evento, reintentarías un cobro que ya entró. El dinero manda sobre el aviso.

<Note>
  Una orden con pago diferido ya emitió su documento fiscal al abrirse, y vuelve a pasar
  por el paso fiscal en `order.completed`. Fire detecta el documento existente y saltea
  la segunda emisión — no te llegan dos facturas.
</Note>

## Idempotencia

Cada pieza se identifica por su `transaction_id` dentro de la orden. Podés reintentar sin miedo:

* **Reenviar el cobro completo** → `duplicate`. No se vuelve a saldar ni a facturar.
* **Reenviar después de un timeout** → reenviá el envío **entero**. Un cobro incompleto nunca se
  registró, y cualquier pieza que sí haya entrado se ignora por su `transaction_id`.

## Validaciones

Todas devuelven `400` salvo donde se indique. Cada rechazo trae un **`code`** además del mensaje:
ramificá por el código, no por el texto — el mensaje está escrito para leerse y se puede reescribir, el
código es contrato.

| regla                                                                        | código                          |
| ---------------------------------------------------------------------------- | ------------------------------- |
| `payments` con al menos una pieza                                            | `400`                           |
| máximo 20 piezas                                                             | `400`                           |
| `details.transaction_status` presente                                        | `400`                           |
| `transaction_id` o `uid` presente                                            | `400`                           |
| `transaction_id` sin repetir dentro del mismo envío                          | `400`                           |
| monto mayor que cero en cada pieza                                           | `400`                           |
| una sola moneda entre las piezas aprobadas                                   | `400 MIXED_CURRENCIES`          |
| la suma de las aprobadas es igual al total de la orden, exacto (todo o nada) | `400 INCOMPLETE_CHARGE`         |
| la suma de las aprobadas no supera el total de la orden                      | `400 AMOUNT_EXCEEDS_TOTAL`      |
| el total de la orden se puede resolver en la moneda de las piezas            | `400 TOTAL_NOT_RESOLVABLE`      |
| API key válida                                                               | `401`                           |
| scope `orders:write` y key vendor-scoped                                     | `403`                           |
| la orden existe dentro de tu vendor                                          | `404`                           |
| la referencia matchea más de una orden                                       | `409 AMBIGUOUS_ORDER_REFERENCE` |
| una pieza nueva sobre una orden ya saldada                                   | `409 ORDER_ALREADY_SETTLED`     |
| una pieza nueva sobre una orden cerrada sin cobrarse                         | `409 ORDER_NOT_OPEN`            |

Los campos que Fire no conoce **se aceptan y se guardan**: podés enviar tu objeto de pago completo
sin recortarlo. La moneda de una pieza rechazada no invalida el envío, porque no suma.

## Respuestas

<ResponseExample>
  ```json 200 — saldada theme={null}
  {
    "success": true,
    "data": {
      "orderId": "7e2b8c10-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
      "orderCode": "OC-1024",
      "outcome": "settled",
      "settled": true,
      "status": "COMPLETED",
      "paymentStatus": "SUCCEEDED",
      "completedAt": "2026-07-28T17:11:02.000Z",
      "tenderCount": 2,
      "declinedCount": 0,
      "amountMismatch": false,
      "declines": [],
      "flowsTriggered": 1
    }
  }
  ```

  ```json 200 — rechazada, la orden sigue abierta theme={null}
  {
    "success": true,
    "data": {
      "orderId": "7e2b8c10-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
      "orderCode": "OC-1024",
      "outcome": "declined",
      "settled": false,
      "status": "OPEN",
      "paymentStatus": "PENDING",
      "completedAt": null,
      "tenderCount": 0,
      "declinedCount": 2,
      "amountMismatch": false,
      "declines": [
        { "sent": "INSUFFICIENT_FUNDS", "resolvedTo": "INSUFFICIENT_FUNDS", "group": "funds", "exceedsOrderTotal": false },
        { "sent": "51", "resolvedTo": null, "group": null, "exceedsOrderTotal": true }
      ],
      "flowsTriggered": 0
    }
  }
  ```

  ```json 200 — reintento del mismo cobro theme={null}
  {
    "success": true,
    "data": {
      "orderId": "7e2b8c10-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
      "orderCode": "OC-1024",
      "outcome": "duplicate",
      "settled": false,
      "status": "COMPLETED",
      "paymentStatus": "SUCCEEDED",
      "completedAt": "2026-07-28T17:11:02.000Z",
      "tenderCount": 0,
      "declinedCount": 0,
      "amountMismatch": false,
      "flowsTriggered": 0
    }
  }
  ```

  ```json 400 — el cobro no llega al total theme={null}
  {
    "success": false,
    "error": "BUSINESS_ERROR",
    "code": "INCOMPLETE_CHARGE",
    "message": "Approved tenders total 200000 does not reach the order total 359000 — send the complete charge in one call"
  }
  ```

  ```json 400 — cobraste más que el total theme={null}
  {
    "success": false,
    "error": "BUSINESS_ERROR",
    "code": "AMOUNT_EXCEEDS_TOTAL",
    "message": "Approved tenders total 500000 exceeds the order total 359000"
  }
  ```

  ```json 409 — la orden ya estaba saldada theme={null}
  {
    "success": false,
    "error": "CONFLICT",
    "code": "ORDER_ALREADY_SETTLED",
    "message": "Order is already settled and does not accept new payment reports",
    "data": {
      "orderId": "7e2b8c10-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
      "orderCode": "OC-1024",
      "completedAt": "2026-07-30T16:24:11.802Z"
    }
  }
  ```

  ```json 409 — la orden está cerrada (cancelada) theme={null}
  {
    "success": false,
    "error": "CONFLICT",
    "code": "ORDER_NOT_OPEN",
    "message": "Order is closed and does not accept payment reports",
    "data": {
      "orderId": "7e2b8c10-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
      "orderCode": "OC-1024",
      "orderStatus": "CANCELLED",
      "completedAt": null
    }
  }
  ```

  ```json 400 — validación theme={null}
  {
    "success": false,
    "error": "VALIDATION_ERROR",
    "message": "Datos de entrada inválidos",
    "details": [
      {
        "code": "custom",
        "path": ["payments", 1],
        "message": "Duplicate transaction id \"BR-K000-POS-38-1785258648480\" within the same payment batch"
      }
    ]
  }
  ```

  ```json 403 — key sin vendor theme={null}
  {
    "success": false,
    "error": "FORBIDDEN",
    "message": "API key must be vendor-scoped (account + vendor binding) to access this endpoint"
  }
  ```

  ```json 404 — la orden no existe en tu vendor theme={null}
  {
    "success": false,
    "error": "NOT_FOUND",
    "message": "InjectedOrder not found with ID ORD-77"
  }
  ```

  ```json 409 — referencia ambigua theme={null}
  {
    "success": false,
    "error": "CONFLICT",
    "message": "External order id ORD-77 matches 2 orders across vendors; cannot disambiguate"
  }
  ```
</ResponseExample>

## Relacionado

<CardGroup cols={2}>
  <Card title="Motivos de rechazo" icon="circle-xmark" href="/es/api-reference/payment-decline-reasons">
    Catálogo agrupado para clasificar los intentos rechazados.
  </Card>

  <Card title="Obtener orden" icon="receipt" href="/es/api-reference/get-order">
    Consultá el estado y el total antes de cobrar.
  </Card>
</CardGroup>
