# Órdenes idempotentes en MT5: nunca dupliques una ejecución al reintentar

URL: https://docs.fxapis.com/es/guides/idempotency

> Por qué reintentar una orden de MetaTrader 5 puede abrir una segunda posición, y cómo lo evita el Idempotency-Key de fxapis: claves de 24 horas, repeticiones…



Toda integración de trading termina enviando la misma orden dos veces. Una conexión móvil
se cae después del toque, un balanceador de carga agota el tiempo de espera, una cola
vuelve a entregar un trabajo, un usuario hace doble clic. En la mayoría de las APIs, una
solicitud duplicada es una molestia. En una API de trading es una segunda posición:
exposición real que nadie eligió.

Esta guía explica de dónde vienen los duplicados, cómo se comporta exactamente el
encabezado `Idempotency-Key` en fxapis y cómo elegir las claves para que un reintento
siempre sea seguro.

## Por qué los reintentos duplican operaciones [#why-retries-duplicate-trades]

Cuando tu código envía una orden, pueden pasar tres cosas: vuelve la respuesta, vuelve un
error, o no vuelve **nada**. El tercer caso es el difícil. La solicitud pudo morir antes de
llegar al broker, o el broker pudo ejecutarla y la respuesta se perdió en el camino de
vuelta. Desde tu lado, ambos casos son indistinguibles.

MetaTrader 5 lo hace concreto. Su servidor de trading responde con códigos de retorno, y
algunos de ellos —un timeout, por ejemplo— describen lo que pasó con la *espera*, no con la
orden. Una orden que "agotó el tiempo de espera" bien puede estar abierta.

Sin una forma de preguntar "¿mi solicitud anterior ya hizo esto?", un cliente tiene dos
malas opciones: reenviar y arriesgar una doble ejecución, o rendirse y arriesgar perder la
operación. La clave de idempotencia es esa pregunta, hecha por el servidor en tu nombre.

## Cómo funciona el Idempotency-Key [#how-the-idempotency-key-works]

Envía cualquier string único, de hasta 200 caracteres, en el encabezado
`Idempotency-Key` de una orden:

```bash
curl -sS -X POST "https://api.fxapis.com/v1/accounts/$ACCOUNT_ID/orders/market" \
  -H "Authorization: Bearer $FXAPIS_KEY" \
  -H "Idempotency-Key: open:signal_9931:member_4821" \
  -H "Content-Type: application/json" \
  -d '{ "symbol": "XAUUSD", "side": "buy", "volume": "0.05" }'
```

Se respeta en las tres llamadas que pueden crear o quitar exposición:
[órdenes de mercado](/api-reference/trading/place-a-market-order),
[órdenes pendientes](/api-reference/trading/place-a-limit-stop-or-stop-limit-order) y
[cerrar una posición](/api-reference/trading/close-a-position). La primera solicitud con una
clave la reclama; todo lo que viene después lo decide ese reclamo:

| Envías                                                             | Recibes                                                                                                                                                                              |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Misma clave, mismo cuerpo, la primera solicitud **terminó**        | El **mismo estado y cuerpo**, repetidos, con el encabezado `idempotent-replay: true`. No se envía nada al broker.                                                                    |
| Misma clave, mismo cuerpo, la primera solicitud **sigue en curso** | **409** `IDEMPOTENCY_IN_FLIGHT`. Espera un momento y envíala de nuevo con la misma clave.                                                                                            |
| Misma clave, cuerpo o endpoint **distinto**                        | **409** `IDEMPOTENCY_KEY_REUSED`. No se coloca nada. Es un error en cómo se generan las claves.                                                                                      |
| Misma clave después de **`SEND_FAILED`**                           | Un intento nuevo de verdad. Está demostrado que la orden nunca llegó al broker, así que la clave se liberó.                                                                          |
| Misma clave en un **cierre**, cuando la posición ya no está        | **404** `POSITION_NOT_FOUND`. Un cierre busca la posición antes de comprobar la clave, así que un cierre que ya se hizo responde esto en lugar de repetir su 201: tómalo como hecho. |

Las claves se recuerdan durante **24 horas** y pertenecen a tu workspace; después, una
clave se libera y una solicitud con ella se trata como nueva. Una clave usada en un
endpoint no se puede usar en otro: abrir y cerrar necesitan claves distintas.

### Qué se repite, exactamente [#what-is-replayed-exactly]

Lo que sea con lo que terminó la primera solicitud. Eso incluye los fallos:

* Un **201** de ejecución se repite como el mismo 201.
* Un **422** `ORDER_REJECTED` se repite como el mismo rechazo. Si el broker lo rechazó porque el precio
  se movió (`retryable: true`) y decides volver a operar, esa es una decisión nueva: usa una clave
  **nueva**.
* Un **503** `ORDER_UNRESOLVED` se repite como el mismo 503. La clave sigue reclamada a propósito: este es
  exactamente el caso en el que una segunda orden debe ser imposible.

Una clave solo se libera cuando sabemos que nada llegó al broker: después de
`SEND_FAILED`, y cuando la solicitud se rechazó antes de que existiera ninguna orden —por
ejemplo, una cuenta que no pudo ponerse en línea a tiempo (`ACCOUNT_NOT_READY`).
Reintentarlas con la misma clave coloca la orden normalmente.

## El resultado desconocido [#the-unknown-outcome]

`ORDER_UNRESOLVED` (503) significa que todavía no sabemos si el broker ejecutó la orden. La
orden se registra en estado `unknown`, y puede estar activa ahora mismo.

**Nunca la reenvíes**: ni con la misma clave (recibirías el mismo 503) y menos con una clave
nueva (así es como ocurren las dobles ejecuciones). En su lugar:

1. Toma `details[0].orderId` del error.
2. Consulta [`GET /v1/orders/{id}`](/api-reference/history/fetch-one-order) cada pocos segundos.
3. Confirmamos el resultado contra los propios registros del broker automáticamente. La orden pasa a
   `filled`, `partially_filled` o `rejected`, según lo que realmente ocurrió.
4. Para confirmarlo de inmediato en lugar de esperar, llama a
   [`POST /v1/accounts/{id}/reconcile`](/api-reference/trading/confirm-pending-results-with-the-broker-now).

Si los registros del broker coinciden con más de una operación, la orden se deja en
`unknown` en lugar de adivinar, y necesita a una persona. `GET /v1/orders?state=unknown`
es la lista sobre la que vale la pena alertar. Los detalles están en
[Trading → Confirmar resultados](/es/trading#confirming-results).

## Un bucle de reintentos que siempre es seguro [#a-retry-loop-that-is-always-safe]

```python
import time, uuid, requests

RETRY_SAME_KEY = {"SEND_FAILED", "IDEMPOTENCY_IN_FLIGHT", "ACCOUNT_NOT_READY", "NO_RUNTIME", "RATE_LIMITED"}

def place_once(session, url, body, key):
    for attempt in range(6):
        try:
            r = session.post(url, json=body, headers={"Idempotency-Key": key}, timeout=90)
        except requests.RequestException:
            time.sleep(min(2 ** attempt, 10))        # no answer seen: same key, ask again
            continue
        if r.status_code == 201:
            return r.json()["data"]
        error = r.json()["error"]
        if error["code"] == "ORDER_UNRESOLVED":
            return {"state": "unknown", "id": error["details"][0]["orderId"]}  # poll it; never resend
        if error["code"] in RETRY_SAME_KEY:
            time.sleep(int(r.headers.get("retry-after", 0)) or min(2 ** attempt, 10))
            continue
        raise RuntimeError(f"{error['code']}: {error['message']}")   # rejected, invalid, quota…
    raise RuntimeError("gave up; the same key is still safe to retry later")
```

La regla que codifica: **la clave se elige una vez, antes del primer intento, y nunca
cambia entre intentos.** Generar un UUID nuevo dentro del bucle anula todo el mecanismo.
Hay programas completos en siete lenguajes en las [guías por lenguaje](/es/guides).

## Elegir las claves [#choosing-keys]

Una clave debe identificar la **decisión de operar**, no la solicitud HTTP. Dos solicitudes
que llevan la misma decisión deben llevar la misma clave; dos decisiones distintas nunca
deben compartir una.

### Una acción del usuario → una clave creada cuando se toma la acción [#one-user-action--a-key-made-when-the-action-is-made]

Para un botón en tu app, crea la clave cuando la orden se crea en tu base de datos, guárdala
en esa fila y envíala en cada intento. Si tu proceso se cae entre el envío y el registro de
la respuesta, el proceso reiniciado encuentra la fila, envía la misma clave y recibe la
respuesta original.

```ts
// When the user confirms — before any request is sent.
const intent = await db.orderIntents.insert({
  userId, accountId, symbol, side, volume,
  idempotencyKey: `intent:${crypto.randomUUID()}`,
});
// Later, and on every retry, from any worker:
await placeOnce(intent.accountId, intent, intent.idempotencyKey);
```

### Una señal que toman muchos miembros → señal + miembro [#a-signal-taken-by-many-members--signal--member]

```
open:signal_9931:member_4821
```

Un miembro que toca dos veces, su teléfono que reintenta y tu worker que reintenta se
reducen a una sola orden. Otro miembro, u otra señal, recibe otra clave. Este es el patrón
que usa [Señales y click-to-trade](/signals).

### Un copiador de operaciones → deal de la maestra + seguidora [#a-trade-copier--master-deal--follower]

```
copy:open:master_deal_5518823:follower_7f3a
copy:close:master_deal_5518901:follower_7f3a
```

El id del deal del broker de la cuenta maestra es único y estable, así que por muchas veces
que tu poller vea el mismo deal, cada seguidora recibe una sola orden por él. Consulta
[Construye un copiador de operaciones de MT5](/guides/copy-trading).

### Cierres y cierres parciales [#closes-and-partial-closes]

Para un cierre completo, `close:{accountId}:{ticket}` es una buena clave. Para cierres
**parciales** de la misma posición, agrega algo que distinga una decisión de la siguiente
—un número de secuencia o el id de la regla que se activó—, porque un segundo cierre parcial
con la misma clave y el mismo cuerpo se repetiría, no se ejecutaría, y uno con la misma
clave pero otro volumen se rechazaría como `IDEMPOTENCY_KEY_REUSED`.

Un cierre repetido cuando la posición ya no está devuelve **404** `POSITION_NOT_FOUND`
("That position is already closed"). Tómalo como hecho, no como un error que reintentar.

### Trabajos de colas [#queue-jobs]

Las colas entregan al menos una vez. Pon la clave en el payload del trabajo cuando se
encola —nunca la generes dentro del trabajo— para que un trabajo reentregado la reutilice.

## Órdenes multicuenta [#multi-account-orders]

Las [órdenes multicuenta](/guides/multi-account-orders) también aceptan un
`Idempotency-Key`. Se guarda con la propia orden: volver a enviar la misma clave devuelve la
orden multicuenta que creó esa clave —**200** con `idempotent-replay: true`— en lugar de crear
otra. A diferencia de las órdenes individuales, el cuerpo no se compara: la misma clave con
otro cuerpo igual devuelve la primera orden multicuenta, no un 409. Esto importa más que en
ningún otro lugar, porque una orden multicuenta duplicada es una operación duplicada en todas
las cuentas a la vez. Usa una clave por decisión, por ejemplo `wave:signal_9931`.

## Lo que la idempotencia no cubre [#what-idempotency-does-not-cover]

* **Otras formas de entrar a la misma cuenta.** Una clave elimina duplicados de las solicitudes a
  fxapis. Si alguien también opera la cuenta desde MetaTrader en el escritorio u otra plataforma, esas
  operaciones son suyas: aparecen en los [deals](/api-reference/history/an-accounts-full-deal-history)
  con `orderId: null`.
* **Claves de más de 24 horas.** Después de ese tiempo, una clave se libera y la misma clave inicia una
  orden nueva. No confíes en ella para eliminar el duplicado de un reintento de la orden de ayer;
  revisa primero `GET /v1/orders`.
* **Mover stops y cancelar.** `modify` y `cancel` no aceptan el encabezado. Los niveles son precios
  absolutos, así que repetir un `modify` o vuelve a fijar los mismos niveles o el broker lo rechaza
  como sin cambios; ninguna de las dos cosas abre ni cierra nada.

## Lista de comprobación [#checklist]

* [ ] Cada orden de mercado, orden pendiente y cierre envía un `Idempotency-Key`.
* [ ] La clave se crea una vez, se guarda con la intención y se reutiliza en cada reintento.
* [ ] Las claves identifican decisiones: señal + miembro, deal de la maestra + seguidora, el id de una intención guardada.
* [ ] Abrir, cerrar y cada cierre parcial tienen claves distintas.
* [ ] `ORDER_UNRESOLVED` nunca se reenvía; la orden se consulta hasta que sale de `unknown`.
* [ ] `ORDER_REJECTED` con `retryable: true` solo se reintenta como una decisión nueva, con una clave nueva.
* [ ] Se monitorea `GET /v1/orders?state=unknown`.

Relacionado: [Errores](/es/errors), [Trading → Los tres fallos](/es/trading#the-three-failures),
y las guías de [Python](/guides/python) y [Node.js](/guides/nodejs) para programas completos.
