Docs

Trading e historial

Órdenes, cierres, órdenes pendientes, órdenes multicuenta y el historial que dejan.

Cómo enviar órdenes, y el registro permanente que dejan.

Si lees una sola sección, lee Los tres fallos. Es la diferencia entre un cliente que reintenta de forma segura y uno que, de vez en cuando, abre dos posiciones donde pediste una.

Enviar una orden de mercado

curl -sS -X POST "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/orders/market" \
  -H "Authorization: Bearer $FXAPIS_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "symbol": "EURUSD",
    "side": "buy",
    "volume": "0.10",
    "stopLoss": "1.0890",
    "takeProfit": "1.1010",
    "deviationPoints": 20,
    "clientOrderId": "strategy-a-0417"
  }'

La cuenta debe estar en línea (ready). Una cuenta en modo warm_on_demand se pone en línea sola para la orden; consulta Cuentas.

Símbolo o instrumento

symbol es el nombre propio del broker para el mercado, exactamente como lo escribe ese broker (XAUUSD.a, US30.cash). Para operar el mismo mercado en cuentas de distintos brokers, envía instrument en su lugar —XAUUSD, US30, BTCUSD— y cada orden opera el símbolo propio de esa cuenta para ese mercado. Consulta Instrumentos y símbolos.

Los volúmenes y los precios son strings

"0.10", no 0.1. Esto es obligatorio: un número JSON se rechaza con INVALID_REQUEST, no se acepta en silencio.

Un número JSON es un double IEEE. 0.1 + 0.2 es 0.30000000000000004, y un volumen que pasó por un double ya perdió una precisión que no podemos recuperar. Aceptarlo significaría operar en silencio un tamaño que no pediste, así que la API lo rechaza mientras todavía puedes corregirlo.

Usa un tipo decimal de tu lado y serialízalo como string.

Los stops son precios absolutos

stopLoss: "1.0890" es un precio, no una distancia en pips. Comprobamos el error que el broker no puede ver —stops del lado equivocado para la dirección que querías— antes de enviar nada, y lo rechazamos con 400 INVALID_REQUEST:

  • El stop loss de una compra debe estar por debajo de su take profit (el de una venta, por encima).
  • Los stops de una orden pendiente se comprueban contra su propio precio o, en una stop-limit, contra su precio límite.
  • El precio de ejecución de una orden de mercado todavía no se conoce, así que sus stops se comprueban contra el mercado —el ask para una compra, el bid para una venta— cuando fxapis tiene un precio reciente del símbolo en esa cuenta; si no, se deja al broker. Un precio reciente es una cotización en vivo ya leída para esa cuenta, con un tick de menos de 10 segundos, o si no, el precio de una posición abierta en el símbolo. La orden nunca espera a que se obtenga un precio.

Las reglas de distancia y de paso son del broker, y las aplica su order_check antes de enviar nada; un stop que rechaza vuelve como ORDER_REJECTED con el motivo del broker.

Envía siempre un Idempotency-Key

Idempotency-Key: 8f14e45f-ea8d-4b1e-9c2a-7b3d5e9f0a11

Sin uno, una solicitud que agota el tiempo de espera te deja atascado. No puedes saber si la orden se envió; reintentar arriesga una segunda posición, y no reintentar arriesga no tener ninguna. Con uno, reintentar no cuesta nada.

SituaciónLo que recibes
Misma clave, mismo cuerpo, la primera llamada terminóLa misma respuesta, repetida, con idempotent-replay: true
Misma clave, mismo cuerpo, la primera llamada sigue en curso409 IDEMPOTENCY_IN_FLIGHT: reintenta en breve
Misma clave, cuerpo distinto409 IDEMPOTENCY_KEY_REUSED
Misma clave, después de un SEND_FAILEDUn intento nuevo de verdad: nada llegó al broker, así que la clave se libera
Misma clave, después de un rechazoSe repite el rechazo. Para volver a intentarlo, usa una clave nueva

Esa última fila es la sutil, y es deliberada. Una clave solo se libera cuando sabemos que el broker nunca vio la orden. Si el resultado no está resuelto, la clave sigue reclamada y un reintento repite la respuesta sin resolver en lugar de enviar una segunda orden.

Las claves se respetan durante 24 horas y su alcance es tu workspace; después, una clave se libera y se puede volver a usar. Usa una nueva por orden: un UUID es ideal.

Los tres fallos

Una orden puede fallar de tres maneras, y la respuesta te dice cuál. Una cuarta respuesta, NO_PRICE, se describe junto con ellas: no se envió nada, pero reintentar de inmediato no puede ayudar.

SEND_FAILED (503): nunca llegó al broker

{"error":{"code":"SEND_FAILED","message":"… the order was never sent. It is safe to retry.",
 "details":[{"orderId":"...","state":"failed","retryable":true}]}}

Sabemos con certeza que no se envió nada al broker. La orden queda failed.

Reinténtala. Es el único estado de orden en el que reenviar es seguro.

ORDER_REJECTED (422): el broker dijo que no

{"error":{"code":"ORDER_REJECTED","message":"The broker rejected the order: No money",
 "details":[{"orderId":"...","state":"rejected","retryable":false}]}}

El broker la rechazó, con un motivo, y no se ejecutó nada. La orden lleva el retcode propio del broker: consúltala con GET /v1/orders/{id} y verás 10019 aquí, sin modificar.

retryable te dice si el motivo era transitorio:

  • retryable: true: una recotización (10004), un precio que se movió (10020), sin cotizaciones en este momento (10021). El mercado se movió; enviar una orden nueva —con un Idempotency-Key nuevo— es lo correcto. Mira primero el nuevo precio.
  • retryable: false: margen insuficiente (10019), mercado cerrado (10018), trading desactivado (10017), un volumen o un stop inválido. Reintentar solo trae el mismo rechazo.

NO_PRICE (422): el broker no tenía precio

{"error":{"code":"NO_PRICE","message":"The broker has no current price for USDJPY on this account, so nothing was sent. …",
 "details":[{"orderId":"...","state":"failed","retryable":false}]}}

La terminal de la cuenta no tenía cotización para el símbolo, así que no envió la orden. Normalmente el mercado está cerrado, o este broker no ofrece el símbolo con ese nombre. La orden queda failed.

No reintentes de inmediato. Revisa el horario de trading del símbolo en GET /v1/accounts/{id}/instruments y reintenta cuando esté cotizando. No se envió nada, así que se puede reutilizar el mismo Idempotency-Key.

ORDER_UNRESOLVED (503): no lo sabemos

{"error":{"code":"ORDER_UNRESOLVED","message":"… we do not know whether the order reached the broker. Do not resend it.",
 "details":[{"orderId":"...","state":"unknown","retryable":false}]}}

No reenvíes esta orden. Puede estar activa en el broker ahora mismo.

Esto ocurre cuando un envío agota el tiempo de espera, la conexión se cae a mitad de la solicitud, o el broker devuelve un código que no dice qué pasó: 10012 TIMEOUT cancela nuestra espera, no necesariamente la orden.

Consulta GET /v1/orders/{id}. Para una orden que abre una posición, confirmamos el resultado con el broker automáticamente y la sacamos de unknown a lo que realmente pasó; needsReconciliation es true hasta entonces. Un cierre, un cambio de stop o una cancelación que termina en unknown no se resuelve automáticamente: su stateDetail te indica revisar la posición o la orden en el broker. Consulta Confirmar resultados más abajo.

Aquí somos cautelosos a propósito. Un resultado que no podemos confirmar es unknown, nunca "probablemente falló", incluso para un código de retorno que nunca habíamos visto. Equivocarse en esta dirección cuesta una espera corta; equivocarse en la otra cuesta una posición que nunca pediste.

Confirmar resultados

Una orden de apertura en unknown se comprueba automáticamente contra el historial de deals del broker, cada minuto. Un cierre, un cambio de stop o una cancelación no produce un deal de apertura con el cual compararse, así que se queda en unknown con un stateDetail que te pide revisar en el broker: interpretar una ausencia como "falló" podría ser un error. POST /v1/accounts/{id}/reconcile lo hace de inmediato para una cuenta.

curl -sS -X POST "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/reconcile" -H "Authorization: Bearer $FXAPIS_KEY"
{"data":{"examined":1,"resolved":1,"stillUnknown":0,"ambiguous":0,
         "dealsIngested":1,"positionsOpen":1,"positionsClosed":0,"skipped":null}}

Se puede llamar en cualquier momento, y llamarlo dos veces no tiene riesgo.

En qué se resuelve

En el brokerLa orden pasa a
Operaciones que cubren todo el volumenfilled
Operaciones que cubren partepartially_filled
Ninguna operación coincide, y ya pasó suficiente tiemporejected
Ninguna operación coincide todavíasigue en unknown por ahora
Más de una coincidencia posiblesigue en unknown, contada como ambiguous

Una orden unknown nunca pasa a failed: ese estado significa que la orden, de forma demostrable, nunca salió de fxapis, y después de los hechos solo el registro del broker puede decir qué pasó.

Cuando no puede estar seguro, no adivina. Si dos operaciones en el broker podrían ser tu orden, la orden sigue en unknown y se cuenta en ambiguous: asignar una ejecución a la orden equivocada produciría un historial que parece correcto y no lo es. Esas órdenes necesitan a una persona:

curl -sS "$FXAPIS_API/v1/orders?state=unknown" -H "Authorization: Bearer $FXAPIS_KEY"

El stateDetail de la orden dice cuántas candidatas había.

Los comentarios de tus órdenes

fxapis agrega una etiqueta corta al inicio del comentario de MT5 de cada orden; así se relaciona el registro del broker con tu orden. El comentario admite 29 caracteres; tu propio comment recibe los 17 que quedan después de la etiqueta, en ASCII simple. Un comentario más largo se recorta para que quepa, nunca se rechaza.

Mantiene tu historial completo

Cada comprobación también registra los deals del broker del período, haya o no algo que confirmar —incluidos los deals que no causó ninguna orden tuya—, y actualiza las posiciones abiertas. Una posición que el broker ya no informa se marca como cerrada, no se elimina.

Cerrar una posición

curl -sS -X POST "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/positions/2104151615/close" \
  -H "Authorization: Bearer $FXAPIS_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" -d '{"volume": "0.05"}'

Omite volume para cerrarla completa.

El ticket a cerrar es el brokerPositionId de la posición. Para una posición que abrió tu propia orden, esa orden también lo lleva, como order.brokerPositionId, tomado de la propia ejecución cuando la orden abre. Una orden pendiente que se ejecuta después también lo recibe, vinculado desde las posiciones y los deals del broker, normalmente en unos minutos, y pasa de working a filled al mismo tiempo (consulta Estados de una orden). Mientras siga en null, busca la posición en GET /v1/accounts/{id}/positions: una posición nueva lleva el ticket de la orden que la abrió, que es el brokerOrderId de esa orden.

Una clave de idempotencia importa más aquí que en una orden de apertura. Un cierre reintentado no cierra dos veces: en una cuenta hedging abre una posición opuesta, y la cuenta termina con una exposición que nadie eligió.

Un cierre es una orden. Tiene su propio registro con intent: "close", los mismos tres modos de fallo y la misma regla: ORDER_UNRESOLVED significa no reenviar. Un cierre sin resolver es el caso en el que podrías creer que no tienes posición y no ser así.

Dos cosas se rechazan antes de enviar nada:

  • Cerrar más de lo que tiene la posición. El mensaje nombra ambos números.
  • Un cierre parcial que dejaría menos del volumen mínimo del símbolo (volumeMin en su lista de símbolos): el broker lo rechaza después del cierre, y deja una posición demasiado pequeña para operar y demasiado pequeña para cerrar. Ciérrala completa, o deja al menos el mínimo.

Mover un stop

curl -sS -X POST "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/positions/2104151615/modify" \
  -H "Authorization: Bearer $FXAPIS_KEY" -H "Content-Type: application/json" \
  -d '{"stopLoss": "1.0890", "takeProfit": null}'

null quita un nivel. Omitir el campo lo deja sin cambios. Que un campo omitido borre un stop en silencio es la forma en que la protección desaparece sin que nadie lo elija.

Se registra con intent: "modify", porque ampliar un stop cambia lo que la cuenta arriesga y pertenece junto a la operación que protege: un stop que se movió antes de una pérdida es exactamente lo que pregunta una auditoría.

Los stops se comprueban contra el precio actual del mercado, no contra la entrada: el stop loss de una compra debe estar por debajo del mercado y su take profit por encima (en una venta, al revés). Así que mover un stop a break-even, o arrastrarlo hacia la ganancia, está permitido. Un stop que el mercado ya superó se rechaza antes de enviar nada, porque cerraría la posición de inmediato. Los stops de una orden nueva se comprueban contra su entrada: consulta Los stops son precios absolutos.

Órdenes pendientes

curl -sS -X POST "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/orders/pending" \
  -H "Authorization: Bearer $FXAPIS_KEY" -H "Content-Type: application/json" \
  -d '{"symbol":"EURUSD","side":"buy","kind":"limit","volume":"0.10","price":"1.0900"}'
TipoLa compra esperaLa venta espera
limitpor debajo del mercadopor encima
stoppor encima del mercadopor debajo
stop_limitel stop se activa y luego coloca una limitigual

Una limit espera un precio mejor; una stop espera uno peor y es la forma de operar una ruptura. Cuando fxapis tiene un precio reciente del símbolo en esa cuenta (consulta Los stops son precios absolutos), el lado equivocado se rechaza antes de enviar nada, con un mensaje que nombra la orden que probablemente querías. Sin él, el broker responde con su propio INVALID_PRICE, que no dice nada sobre en qué sentido debía ir.

Las órdenes pendientes son parte de los planes de pago; en un plan sin ellas, la solicitud se rechaza con 402 FEATURE_NOT_IN_PLAN.

Para mover una orden pendiente activa —su precio, sus stops, su precio stop-limit o su vencimiento— usa POST /v1/orders/{id}/modify. Conserva el ticket de la orden, así que conserva su lugar en la cola del broker; un campo omitido no se toca, y "stopLoss": null quita un stop. Solo se puede mover una orden working.

Una orden pendiente colocada está working, no filled. Esa distinción es dinero real: filled significa que existe una posición. Algunos brokers responden a la colocación de una pendiente con DONE, el mismo código que devuelve una ejecución, e interpretarlo como ejecución te diría que estás en el mercado cuando estás esperando entrar.

Cancela una con POST /v1/orders/{id}/cancel. Si ya se activó, el broker la rechaza con 10035: en ese punto hay una posición, no una orden, y lo que quieres es un cierre. Una cancelación exitosa pasa la orden pendiente a cancelled y registra la propia instrucción como completed.

Margen y ganancia, antes de operar

curl -sS -X POST "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/calculate" \
  -H "Authorization: Bearer $FXAPIS_KEY" -H "Content-Type: application/json" \
  -d '{"kind":"margin","symbol":"EURUSD","side":"buy","volume":"0.10","price":"1.1370"}'
# → { "data": { "kind": "margin", "value": "22.74000000" } }

No abre nada y se puede llamar libremente. La cuenta debe estar en línea: una fuera de línea se rechaza con 409 NO_RUNTIME. Así averiguas que una orden será rechazada por margen antes de enviarla, en lugar de recibir un 10019. "kind": "profit" necesita un closePrice.

Un value nulo significa que el broker no quiso responder, lo que no es lo mismo que cero.

Precios en vivo

curl -sS "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/quotes?symbols=EURUSD&instruments=XAUUSD" \
  -H "Authorization: Bearer $FXAPIS_KEY"
# → { "data": [
#     { "symbol": "EURUSD", "instrument": null, "status": "ok", "bid": "1.08450000", "ask": "1.08462000",
#       "spread": 12, "digits": 5, "brokerTime": "2026-10-15 15:00:00.123", "time": "2026-10-15T12:00:00.123Z",
#       "observedAt": "2026-10-15T12:00:00.300Z", "ageMs": 177, "stale": false, "marketOpen": true, "tradeMode": "full" },
#     { "symbol": "XAUUSD.a", "instrument": "XAUUSD", "status": "ok", "bid": "2650.10000000", … } ] }

Los precios vienen del propio broker de la cuenta. Cada broker cotiza distinto, y estos son los precios a los que opera esta cuenta.

  • Qué puedes pedir: hasta 20 symbols del broker e instruments en total, separados por comas, todos leídos en una llamada. Un instrumento se cotiza como el símbolo propio de esta cuenta para ese mercado. La cuenta debe estar en línea: ready, executing o degraded (si no, 409 ACCOUNT_NOT_READY).
  • Cada cotización con un tick lleva time (UTC), brokerTime (el reloj propio del broker, como lo muestra su terminal) y ageMs, la antigüedad del tick cuando se leyó la terminal en observedAt. Aplica tu propia regla de frescura a partir de esos datos.
  • Un mercado cerrado responde su último precio con marketOpen: false y stale: false, nunca con un error.
  • stale es true solo mientras el mercado está abierto y el último tick tiene más de 60 segundos.
  • marketOpen viene de las sesiones de trading propias del broker. Es null (y stale es false) hasta que la terminal las haya informado, más o menos un minuto después de que una cuenta se pone en línea por primera vez.
  • tradeMode es lo que el broker permite en el símbolo en este momento: full, longOnly, shortOnly, closeOnly o disabled. closeOnly y disabled no permiten posiciones nuevas.
  • Un símbolo que este broker no tiene responde status: "unknown_symbol" en su lugar; uno del que todavía no envió precio, status: "no_price".
  • Consultas repetidas: no hay stream. Las respuestas se comparten durante un cuarto de segundo, así que una cotización puede haberse leído hasta 250 ms antes de que la pidieras: esa es la antigüedad de la lectura, no del precio, que es lo que te dicen ageMs y time. A la terminal de cada cuenta se le pueden hacer 300 consultas por minuto con ráfagas de 50, sumando todos los llamadores y todas las claves; las respuestas compartidas son gratis. Por encima de eso: 429 RATE_LIMITED con retry-after.
  • Una terminal que todavía corre un worker anterior —algo raro— responde 501 CAPABILITY_UNAVAILABLE en su lugar. No lo reintentes; mientras tanto, el currentPrice de una posición abierta es el precio.

Los stops de una orden se comprueban contra este precio cuando se leyó uno recientemente: el ask para una compra, el bid para una venta (Los stops son precios absolutos).

Historial de precios

Disponible después del próximo mantenimiento programado. Hasta que la terminal de la cuenta se haya actualizado, esto responde 501 CAPABILITY_UNAVAILABLE.

curl -sS "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/candles?instrument=XAUUSD&timeframe=H1&count=100" \
  -H "Authorization: Bearer $FXAPIS_KEY"
# → { "data": { "symbol": "XAUUSD.a", "instrument": "XAUUSD", "timeframe": "H1", "digits": 2, "candles": [
#     { "time": "2026-10-23T13:00:00Z", "open": "2650.10", "high": "2655.25", "low": "2648.00",
#       "close": "2652.50", "tickVolume": 1234, "spread": 35 }, … ] } }

Barras (velas) del propio broker de la cuenta, para un symbol o un instrument.

  • timeframe: M1, M5, M15, M30, H1, H4, D1, W1 o MN1. count: de 1 a 1,000 barras (100 por defecto), contando hacia atrás desde la actual. De la más antigua a la más reciente; la última barra todavía se está formando.
  • time es cuándo abrió la barra, en UTC, convertido con el desfase de reloj actual del broker: las barras anteriores al último cambio de horario de verano del broker pueden tener una hora de diferencia.
  • tickVolume cuenta los cambios de precio en la barra, no el volumen operado. spread está en puntos.
  • La cuenta debe estar en línea (si no, 409 ACCOUNT_NOT_READY). Un símbolo que el broker no tiene responde 404 SYMBOL_NOT_FOUND. Cada llamada llega a la terminal y cuenta dentro de las mismas 300 por minuto que las cotizaciones.

Estados de una orden

EstadoSignificado¿Reenviar?
acceptedRecibida, todavía no enviada—
sendingEn cursoNo
workingUna orden pendiente esperando en el broker. No existe ninguna posición.No: ya está ahí
completedTerminó una instrucción que no es una operación: una cancelación, un stop movido—
filledEl broker confirmó todo el volumenNo
partially_filledEjecutada en parte; el resto aún puede ejecutarseNo
rejectedEl broker la rechazó. No se ejecutó nada.Solo si es retryable
failedDemostrado que nunca llegó al brokerSí
unknownResultado no establecidoNunca
cancelledRetirada antes de ejecutarse—
expiredUna orden pendiente venció en el broker—

failed y unknown nunca son intercambiables. failed es algo que afirmamos solo con pruebas.

Una orden pendiente que se activa pasa de working a filled (o partially_filled) cuando su ejecución aparece en el historial de deals del broker o como una posición con su ticket, normalmente en unos minutos, con un único webhook order.filled. filledPrice y filledVolume vienen de su deal de ejecución; cuando solo se conoce la posición, se toman de ella en una cuenta hedging (el volumen solo mientras siga siendo igual al de la orden) y se dejan en null en una cuenta netting, cuya posición puede contener ejecuciones de otras órdenes, hasta que llega el deal. Una orden pendiente que el broker cancela o deja vencer por su cuenta todavía no se detecta: se queda en working.

Historial

Tres tablas, porque las preguntas son distintas.

Órdenes: lo que pediste

curl -sS "$FXAPIS_API/v1/orders?state=unknown&limit=50" -H "Authorization: Bearer $FXAPIS_KEY"

Cada orden, con el código de retorno del broker conservado exactamente como llegó. Filtra por accountId, state, symbol y una ventana since/until.

state=unknown es la consulta sobre la que vale la pena construir una alerta: es todo lo que todavía no tiene un resultado establecido.

Deals: lo que el broker realmente hizo

curl -sS "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/deals?since=2026-09-01T00:00:00Z" \
  -H "Authorization: Bearer $FXAPIS_KEY"

Un deal es el registro del broker de dinero que cambia de manos. Es contra lo que se concilia un estado de cuenta, y no es una fila por orden:

  • Una orden puede producir varios deals. Una ejecución parcial deja más de uno.
  • Un deal puede existir sin ninguna orden tuya detrás. Un stop loss que se activa en el broker, un cargo de swap, un depósito, una corrección, o alguien que opera el mismo login desde MetaTrader en el escritorio.

Ese segundo punto es la razón por la que este endpoint existe por separado. Un historial que solo contuviera tus propias órdenes mostraría una cuenta cuyo balance se mueve sin motivo visible. orderId es null exactamente en esas filas, para que puedas distinguirlas.

Ordenados por la marca de tiempo propia del broker, no la nuestra. dealType tiene nombre en lugar de número —balance, commission, buy— porque 2 no significa nada en un estado de cuenta.

Deals de una orden: GET /v1/orders/{id}/deals.

Posiciones: lo que está abierto ahora

curl -sS "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/positions" -H "Authorization: Bearer $FXAPIS_KEY"

Una instantánea con observedAt indicando cuándo era cierta. El broker es la fuente de verdad; esto es lo último que vimos, y la marca de tiempo está ahí para que puedas distinguir una vista actual de una desactualizada en lugar de suponerlo.

Paginación

Por cursor, no por offset:

curl -sS "$FXAPIS_API/v1/orders?limit=100" -H "Authorization: Bearer $FXAPIS_KEY"
# → { "data": [...], "page": { "hasMore": true, "nextCursor": "2026-09-28T14:02:11.482Z" } }

curl -sS "$FXAPIS_API/v1/orders?limit=100&cursor=2026-09-28T14:02:11.482Z" -H "Authorization: Bearer $FXAPIS_KEY"

Un offset se desplaza a medida que llegan órdenes nuevas, así que la segunda página se saltaría filas en silencio: en un historial de trading, justamente las filas agregadas mientras paginabas.

Retención

Las órdenes y los deals se conservan durante la vida de la cuenta y no se depuran. Desconectar una cuenta borra sus credenciales; no borra su historial, porque ese historial es con lo que se resuelve una disputa o una auditoría.

Un cliente de ejemplo

import uuid, time, httpx

class OrderError(Exception): ...
class Unresolved(OrderError):
    """Do not resend. The order may be live."""

def place(client, account_id, **order):
    key = str(uuid.uuid4())

    for attempt in range(3):
        response = client.post(
            f"/v1/accounts/{account_id}/orders/market",
            json=order,
            # The SAME key on every attempt. That is the whole point: the
            # server decides whether this is a retry or a new order, and it is
            # the only party that can know.
            headers={"Idempotency-Key": key},
        )

        if response.status_code == 201:
            return response.json()["data"]

        error = response.json()["error"]
        code = error["code"]

        if code == "SEND_FAILED":
            # Proven not to have reached the broker.
            time.sleep(2 ** attempt)
            continue

        if code == "IDEMPOTENCY_IN_FLIGHT":
            time.sleep(1)
            continue

        if code == "ORDER_UNRESOLVED":
            # Never retried. Hand the order id to whatever follows up on
            # unresolved orders, and stop.
            raise Unresolved(error["details"][0]["orderId"])

        if code == "ORDER_REJECTED":
            if error["details"][0].get("retryable"):
                # The market moved: a new decision needs a NEW key. Resending
                # with this one would only replay the rejection.
                raise OrderError(f"retryable rejection: {error['message']}")
            raise OrderError(error["message"])

        raise OrderError(f"{code}: {error['message']}")

    raise OrderError("gave up after 3 attempts")

La idea clave del ejemplo: se usa la misma clave en cada intento, porque el servidor es el único que puede saber si se trata de un reintento o de una orden nueva; ORDER_UNRESOLVED nunca se reintenta; y un rechazo retryable necesita una decisión nueva con una clave nueva.

Órdenes multicuenta

Una operación, enviada en muchas cuentas en el mismo momento. La API las llama execution waves.

curl -sS -X POST "$FXAPIS_API/v1/execution-waves" \
  -H "Authorization: Bearer $FXAPIS_KEY" -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" -d '{
    "accountIds": ["…", "…", "…"],
    "symbol": "EURUSD", "side": "buy", "volume": "0.01",
    "weights": {"<account-id>": "0.05"},
    "barrierPolicy": "release-ready",
    "executeAt": "2026-10-01T13:30:00Z"
  }'

Primero se pone en línea cada cuenta; luego todas las órdenes salen juntas. Consulta GET /v1/execution-waves/{id} para ver el resultado.

weights define el tamaño de la operación por cuenta, para cuentas con balances distintos.

Cada cuenta informa su propio resultado

Los brokers son independientes entre sí, así que cien cuentas no pueden ejecutarse como una sola transacción: las ejecuciones llegan a precios ligeramente distintos, algunas cuentas pueden rechazarse por margen y, de vez en cuando, una queda sin respuesta.

Por eso una orden multicuenta nunca informa un único éxito o fallo. Se liquida, y summary cuenta lo que pasó:

{"state":"settled","dispatchSpreadMs":30,
 "summary":{"total":100,"filled":97,"rejected":2,"skipped":0,"unresolved":1}}

La operación de cada cuenta tiene su propio orderId y es una orden normal: se consulta, se confirma y se audita como cualquier otra. unresolved significa lo mismo que en una orden individual: no reenvíes, la estamos confirmando con el broker.

Cuando algunas cuentas no están en línea a tiempo

barrierPolicyQué pasa
release-readyOpera las cuentas que están en línea; omite el resto. Una entrada parcial es mejor que una tardía.
all-or-nothingNo opera ninguna a menos que todas las cuentas estén en línea. Para estrategias que solo tienen sentido en el conjunto completo.
waitEspera después de executeAt hasta que todas las cuentas estén en línea, hasta expiresAt.

all-or-nothing se refiere al envío, no al resultado: un broker todavía puede rechazar la orden de una cuenta después de que se envió.

Con executeAt, las órdenes salen a esa hora, no antes. Sin él, salen en cuanto todas las cuentas están en línea: se espera a las cuentas que todavía están iniciando sesión, hasta 60 segundos, y después decide la política.

Dispersión

dispatchSpreadMs es el tiempo entre la salida de la orden de la primera cuenta y la de la última. Es el número sobre el que vale la pena alertar: una dispersión creciente significa que las cuentas están obteniendo precios distintos. Cada orden multicuenta informa la suya; estas son de nuestro benchmark contra un broker instantáneo, así que un broker real agrega su propia latencia encima:

cuentasdispersión
1017 ms
5026 ms
10030 ms

Medido de nuestro lado; cada broker agrega su propia latencia encima.

Cancelar una

POST /v1/execution-waves/{id}/cancel, pero solo antes de que haya salido alguna orden. Una vez que las órdenes se están enviando, no se pueden recuperar.

En esta página