# FITAL API, documentación completa # API de FITAL Paga, cobra y concilia desde tus sistemas con una sola cuenta. Con la API de FITAL tu sistema paga a proveedores, cobra a clientes, recibe depósitos y concilia cada movimiento. Todo se opera en HTTPS con JSON, con una sola cuenta por empresa. | Ambiente | Base | Para qué | |---|---|---| | Simulador público | `https://widget.fital.xyz/explorar/v1` | Explorar sin cuenta, con ejemplos fijos | | Cuenta de prueba | `https://widget.fital.xyz/sandbox/v1` | Probar tu integración con datos ficticios | | Producción | `https://widget.fital.xyz/v1` | Mover dinero real | ## Tres caminos - **Quiero verlo funcionar.** Sigue [Tu primer pago de prueba](/docs/primer-pago/). No necesitas cuenta. - **Quiero probar con estado real.** Sigue el [inicio rápido de la cuenta de prueba](/docs/sandbox/inicio-rapido/). - **Quiero recibir eventos.** Sigue el [inicio rápido de webhooks](/docs/webhooks/inicio-rapido/). - **Quiero integrar.** Empieza por [Conceptos](/docs/conceptos/), [Cobertura](/docs/cobertura/), [Autenticación](/docs/autenticacion/) e [Idempotencia](/docs/idempotencia/). - **Quiero consultar una operación.** Ve a la [referencia por operación](/docs/operaciones/), a la [referencia interactiva](/docs/referencia/) o a su [versión en texto](/docs/referencia.md). ## Lo que puedes hacer hoy | Necesidad | Operaciones | Guía | |---|---|---| | Consultar saldos, wallets, cuentas y límites | [`GET /v1/balances`](/docs/operaciones/listar-saldos/), [`GET /v1/wallets`](/docs/operaciones/listar-wallets/), [`GET /v1/receiving-accounts`](/docs/operaciones/listar-cuentas-para-recibir/), [`GET /v1/limits`](/docs/operaciones/consultar-limites/) | [Conceptos](/docs/conceptos/) | | Abrir una cuenta para recibir | [`POST /v1/receiving-accounts`](/docs/operaciones/abrir-cuenta-para-recibir/) | [Cuentas para recibir](/docs/recetas/cuentas-para-recibir/) | | Registrar a quién pagas | [`POST /v1/beneficiaries`](/docs/operaciones/crear-beneficiario/) | [Pago SPEI](/docs/recetas/pago-spei/) | | Pagar | [`POST /v1/quotes`](/docs/operaciones/crear-cotizacion/), [`POST /v1/payments`](/docs/operaciones/crear-pago/) | [Cobertura](/docs/cobertura/), con una guía por riel | | Cobrar | [`POST /v1/collections`](/docs/operaciones/crear-cobro/) | [Cobros](/docs/recetas/cobros/) | | Recibir por dirección de depósito | [`POST /v1/deposit-addresses`](/docs/operaciones/crear-direccion-de-deposito/) | [Direcciones de depósito](/docs/recetas/direcciones-deposito/) | | Saber quién te pagó | [`GET /v1/deposits`](/docs/operaciones/listar-depositos/) | [Quién pagó](/docs/recetas/quien-pago/) | | Conciliar | [`GET /v1/movements`](/docs/operaciones/listar-movimientos/), [`GET /v1/movements/{id}/receipt`](/docs/operaciones/descargar-recibo/) | [Movimientos y recibos](/docs/recetas/movimientos-y-recibos/) | ## Para asistentes de IA y herramientas Cada página existe también en Markdown: agrega `.md` a su ruta. El índice está en [`/llms.txt`](https://merchants.fital.xyz/llms.txt) y el contrato en [`/openapi.json`](https://merchants.fital.xyz/openapi.json). Ver [Herramientas](/docs/herramientas/). ## Errores comunes - Usar una llave de un ambiente en la base de otro: responde `401 wrong_environment`. - Pegar una llave real en la referencia interactiva. El botón de prueba sólo habla con el simulador público. ## Qué sigue [Tu primer pago de prueba](/docs/primer-pago/). --- # Tu primer pago de prueba Cinco pasos contra el simulador público, sin cuenta y sin dinero. Al terminar habrás consultado un saldo, creado un pago y leído su estado final, todo contra el simulador público. No necesitas cuenta. El simulador responde ejemplos fijos: enseña la forma de la API, no guarda nada entre llamadas. ## 1. Prepara tus variables ```bash export FITAL_API="https://widget.fital.xyz/explorar/v1" export FITAL_KEY="sk_test_sandbox_publica" export FITAL_SIGNING_SECRET="secreto_de_firma_de_prueba_publico" ``` Esta llave y este secreto son públicos y sólo sirven en el simulador. Tus llaves reales nunca van aquí. ## 2. Consulta tus saldos ```bash curl -sS "$FITAL_API/balances" -H "Authorization: Bearer $FITAL_KEY" ``` ```js const r = await fetch(`${process.env.FITAL_API}/balances`, { headers: { Authorization: `Bearer ${process.env.FITAL_KEY}` }, }); console.log(r.status, await r.json()); ``` ```python import json, os, urllib.error, urllib.request pet = urllib.request.Request(os.environ["FITAL_API"] + "/balances", headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"]}) try: with urllib.request.urlopen(pet) as r: print(r.status, json.load(r)) except urllib.error.HTTPError as e: print(e.code, e.read().decode()) ``` ```php true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('FITAL_KEY')], ]); echo curl_exec($c), "\n"; ``` ## 3. Crea un pago Un pago es una escritura de dinero: lleva `Idempotency-Key` y firma. La clave sale de tu identificador interno, para que un reinicio de tu proceso repita la misma clave y no pague dos veces. ```bash CUERPO='{"beneficiary_id":"ben_3kTMd9wQp2LxZ7vR1nYcA4bE","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"}' CLAVE="pago-factura-1042" TS=$(date +%s) HASH=$(printf '%s' "$CUERPO" | openssl dgst -sha256 -r | cut -d' ' -f1) FIRMA=$(printf '%s\n%s\n%s\n%s\n%s' "$TS" POST /v1/payments "$HASH" "$CLAVE" \ | openssl dgst -sha256 -hmac "$FITAL_SIGNING_SECRET" -r | cut -d' ' -f1) curl -sS -X POST "$FITAL_API/payments" \ -H "Authorization: Bearer $FITAL_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $CLAVE" \ -H "X-Fital-Timestamp: $TS" \ -H "X-Fital-Signature: v1=$FIRMA" \ -d "$CUERPO" ``` ```js import { createHash, createHmac } from 'node:crypto'; const cuerpo = JSON.stringify({"beneficiary_id":"ben_3kTMd9wQp2LxZ7vR1nYcA4bE","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"}); const clave = 'pago-factura-1042'; const ts = String(Math.floor(Date.now() / 1000)); const hash = createHash('sha256').update(cuerpo).digest('hex'); const firma = createHmac('sha256', process.env.FITAL_SIGNING_SECRET) .update([ts, 'POST', '/v1/payments', hash, clave].join('\n')).digest('hex'); const r = await fetch(`${process.env.FITAL_API}/payments`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.FITAL_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': clave, 'X-Fital-Timestamp': ts, 'X-Fital-Signature': `v1=${firma}`, }, body: cuerpo, }); console.log(r.status, await r.json()); ``` ```python import hashlib, hmac, json, os, time, urllib.error, urllib.request datos = {"beneficiary_id":"ben_3kTMd9wQp2LxZ7vR1nYcA4bE","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"} cuerpo = json.dumps(datos).encode() clave = "pago-factura-1042" ts = str(int(time.time())) canon = "\n".join([ts, "POST", "/v1/payments", hashlib.sha256(cuerpo).hexdigest(), clave]) firma = hmac.new(os.environ["FITAL_SIGNING_SECRET"].encode(), canon.encode(), hashlib.sha256).hexdigest() pet = urllib.request.Request(os.environ["FITAL_API"] + "/payments", method="POST", data=cuerpo, headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"], "Content-Type": "application/json", "Idempotency-Key": clave, "X-Fital-Timestamp": ts, "X-Fital-Signature": "v1=" + firma}) try: with urllib.request.urlopen(pet) as r: print(r.status, json.load(r)) except urllib.error.HTTPError as e: print(e.code, e.read().decode()) ``` ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('FITAL_KEY'), 'Content-Type: application/json', 'Idempotency-Key: ' . $clave, 'X-Fital-Timestamp: ' . $ts, 'X-Fital-Signature: v1=' . $firma, ], CURLOPT_POSTFIELDS => $cuerpo, ]); echo curl_exec($c), "\n"; ``` Un `202` con `processing` significa que el pago se aceptó y sigue en curso. No es un fallo. ## 4. Consulta el pago Con `X-Fital-Scenario` pides al simulador la respuesta que quieres practicar. Aquí, el estado final: ```bash curl -sS "$FITAL_API/payments/pay_6nRtY1wKs3QzD8cL5mVhF2gB" \ -H "Authorization: Bearer $FITAL_KEY" \ -H "X-Fital-Scenario: payment_completed" ``` ```js const r = await fetch(`${process.env.FITAL_API}/payments/pay_6nRtY1wKs3QzD8cL5mVhF2gB`, { headers: { Authorization: `Bearer ${process.env.FITAL_KEY}`, 'X-Fital-Scenario': 'payment_completed' }, }); console.log(r.status, await r.json()); ``` ```python import json, os, urllib.error, urllib.request pet = urllib.request.Request(os.environ["FITAL_API"] + "/payments/pay_6nRtY1wKs3QzD8cL5mVhF2gB", headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"], "X-Fital-Scenario": "payment_completed"}) try: with urllib.request.urlopen(pet) as r: print(r.status, json.load(r)) except urllib.error.HTTPError as e: print(e.code, e.read().decode()) ``` ```php true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('FITAL_KEY'), 'X-Fital-Scenario: payment_completed'], ]); echo curl_exec($c), "\n"; ``` ## 5. Practica un error Repite el `POST` del paso 3 con un encabezado más, `X-Fital-Scenario: insufficient_funds`, y otra clave de idempotencia. Va en el `POST` porque el saldo se revisa al crear el pago: en el `GET` del paso 4 este escenario no aplica. ```bash CUERPO='{"beneficiary_id":"ben_3kTMd9wQp2LxZ7vR1nYcA4bE","amount":"1500.00","currency":"MXN","external_ref":"factura-1043"}' CLAVE="pago-factura-1043" TS=$(date +%s) HASH=$(printf '%s' "$CUERPO" | openssl dgst -sha256 -r | cut -d' ' -f1) FIRMA=$(printf '%s\n%s\n%s\n%s\n%s' "$TS" POST /v1/payments "$HASH" "$CLAVE" \ | openssl dgst -sha256 -hmac "$FITAL_SIGNING_SECRET" -r | cut -d' ' -f1) curl -sS -X POST "$FITAL_API/payments" \ -H "Authorization: Bearer $FITAL_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $CLAVE" \ -H "X-Fital-Timestamp: $TS" \ -H "X-Fital-Signature: v1=$FIRMA" \ -H "X-Fital-Scenario: insufficient_funds" \ -d "$CUERPO" ``` ```js import { createHash, createHmac } from 'node:crypto'; const cuerpo = JSON.stringify({"beneficiary_id":"ben_3kTMd9wQp2LxZ7vR1nYcA4bE","amount":"1500.00","currency":"MXN","external_ref":"factura-1043"}); const clave = 'pago-factura-1043'; const ts = String(Math.floor(Date.now() / 1000)); const hash = createHash('sha256').update(cuerpo).digest('hex'); const firma = createHmac('sha256', process.env.FITAL_SIGNING_SECRET) .update([ts, 'POST', '/v1/payments', hash, clave].join('\n')).digest('hex'); const r = await fetch(`${process.env.FITAL_API}/payments`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.FITAL_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': clave, 'X-Fital-Timestamp': ts, 'X-Fital-Signature': `v1=${firma}`, 'X-Fital-Scenario': 'insufficient_funds', }, body: cuerpo, }); console.log(r.status, await r.json()); ``` ```python import hashlib, hmac, json, os, time, urllib.error, urllib.request datos = {"beneficiary_id":"ben_3kTMd9wQp2LxZ7vR1nYcA4bE","amount":"1500.00","currency":"MXN","external_ref":"factura-1043"} cuerpo = json.dumps(datos).encode() clave = "pago-factura-1043" ts = str(int(time.time())) canon = "\n".join([ts, "POST", "/v1/payments", hashlib.sha256(cuerpo).hexdigest(), clave]) firma = hmac.new(os.environ["FITAL_SIGNING_SECRET"].encode(), canon.encode(), hashlib.sha256).hexdigest() pet = urllib.request.Request(os.environ["FITAL_API"] + "/payments", method="POST", data=cuerpo, headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"], "Content-Type": "application/json", "Idempotency-Key": clave, "X-Fital-Timestamp": ts, "X-Fital-Signature": "v1=" + firma, "X-Fital-Scenario": "insufficient_funds"}) try: with urllib.request.urlopen(pet) as r: print(r.status, json.load(r)) except urllib.error.HTTPError as e: print(e.code, e.read().decode()) ``` ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('FITAL_KEY'), 'Content-Type: application/json', 'Idempotency-Key: ' . $clave, 'X-Fital-Timestamp: ' . $ts, 'X-Fital-Signature: v1=' . $firma, 'X-Fital-Scenario: insufficient_funds', ], CURLOPT_POSTFIELDS => $cuerpo, ]); echo curl_exec($c), "\n"; ``` Responde `422` con `error.code` en `insufficient_funds` y `retryable` en `false`: decide con `code` y guarda `request_id`. La lista completa de escenarios, y a qué operación va cada uno, está en [Ambientes](/docs/ambientes/#escenarios). ## Errores comunes - `401 invalid_signature` en la cuenta de prueba o en producción: la firma cubre el cuerpo exacto que envías. Firma los mismos bytes. - Cambiar la clave de idempotencia al reintentar: crea otra operación. Reintenta siempre con la misma. ## Qué sigue Prueba con estado real en el [inicio rápido de la cuenta de prueba](/docs/sandbox/inicio-rapido/). Necesitas acceso al panel, en [merchants.fital.xyz](https://merchants.fital.xyz): entran las empresas con KYB aprobado en [empresa.fital.xyz](https://empresa.fital.xyz). Si tu empresa aún no tiene cuenta, regístrala ahí y completa el KYB. Entiende el modelo en [Conceptos](/docs/conceptos/) y las diferencias entre ambientes en [Ambientes](/docs/ambientes/). --- # Inicio rápido de la cuenta de prueba Tu primer pago ficticio con estado real, firmado y con su webhook. Al terminar habrás creado un pago ficticio en tu cuenta de prueba, lo habrás visto llegar a `completed` y sabrás forzar otros desenlaces. Nada mueve dinero. Si aún no tienes cuenta, empieza por [Tu primer pago de prueba](/docs/primer-pago/), que corre sin registro. Necesitas acceso al panel, en [merchants.fital.xyz](https://merchants.fital.xyz): entran las empresas con KYB aprobado en [empresa.fital.xyz](https://empresa.fital.xyz). Si tu empresa aún no tiene cuenta, regístrala ahí y completa el KYB. ## 1. Crea tu llave En el panel elige el ambiente **Sandbox** y, en la pantalla Llaves, **Crear llave de API**. Dale al menos estos scopes: `balances:read`, `beneficiaries:read`, `payments:read` y `payments:write`. Guarda la llave y su secreto de firma: se muestran una sola vez. ## 2. Prepara tus variables ```bash export FITAL_API="https://widget.fital.xyz/sandbox/v1" export FITAL_KEY="sk_test_fital_..." export FITAL_SIGNING_SECRET="el secreto de firma de esa llave" ``` ## 3. Mira tu saldo ficticio ```bash curl -sS "$FITAL_API/balances" -H "Authorization: Bearer $FITAL_KEY" ``` ```js const r = await fetch(`${process.env.FITAL_API}/balances`, { headers: { Authorization: `Bearer ${process.env.FITAL_KEY}` }, }); console.log(r.status, await r.json()); ``` ```python import json, os, urllib.error, urllib.request pet = urllib.request.Request(os.environ["FITAL_API"] + "/balances", headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"]}) try: with urllib.request.urlopen(pet) as r: print(r.status, json.load(r)) except urllib.error.HTTPError as e: print(e.code, e.read().decode()) ``` ```php true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('FITAL_KEY')], ]); echo curl_exec($c), "\n"; ``` Tu cuenta nace con saldo ficticio en MXN y USD. ## 4. Elige un beneficiario La cuenta ya trae beneficiarios aprobados. Cada uno termina sus pagos de una forma: el nombre lo dice. ```bash curl -sS "$FITAL_API/beneficiaries" \ -H "Authorization: Bearer $FITAL_KEY" ``` ```js const r = await fetch(`${process.env.FITAL_API}/beneficiaries`, { headers: { Authorization: `Bearer ${process.env.FITAL_KEY}` }, }); console.log(r.status, await r.json()); ``` ```python import json, os, urllib.error, urllib.request pet = urllib.request.Request(os.environ["FITAL_API"] + "/beneficiaries", headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"]}) try: with urllib.request.urlopen(pet) as r: print(r.status, json.load(r)) except urllib.error.HTTPError as e: print(e.code, e.read().decode()) ``` ```php true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('FITAL_KEY')], ]); echo curl_exec($c), "\n"; ``` | Titular | Sus pagos terminan en | |---|---| | `PROVEEDOR FICTICIO ÉXITO` | `completed` | | `PROVEEDOR FICTICIO RECHAZO` | `failed` | | `PROVEEDOR FICTICIO DEVOLUCIÓN` | `completed` y luego `returned` | | `PROVEEDOR FICTICIO REVISIÓN` | `in_review` y luego `completed` | | `PROVEEDOR FICTICIO INCIERTO` | `processing` sin confirmar y luego `completed`, unos 5 minutos después | Mientras el pago a `PROVEEDOR FICTICIO INCIERTO` sigue sin confirmar, cualquier otro pago de tu empresa responde `409 payment_in_flight`. Para seguir sin esperar, llévalo a `completed` con [`POST /sandbox/v1/payments/{id}/advance`](/docs/operaciones/avanzar-pago-de-prueba/). La respuesta da sólo el `id`: la pantalla Sandbox del panel muestra el nombre de cada uno. Guarda en `BENEFICIARIO` el `id` del de éxito: ```bash export BENEFICIARIO="ben_..." ``` ## 5. Crea un pago ```bash REF="mi-primer-pago" CUERPO=$(printf '{"beneficiary_id":"%s","amount":"100.00","currency":"MXN","external_ref":"%s"}' "$BENEFICIARIO" "$REF") CLAVE="pago-$REF-$BENEFICIARIO" TS=$(date +%s) HASH=$(printf '%s' "$CUERPO" | openssl dgst -sha256 -r | cut -d' ' -f1) FIRMA=$(printf '%s\n%s\n%s\n%s\n%s' "$TS" POST /v1/payments "$HASH" "$CLAVE" \ | openssl dgst -sha256 -hmac "$FITAL_SIGNING_SECRET" -r | cut -d' ' -f1) curl -sS -X POST "$FITAL_API/payments" \ -H "Authorization: Bearer $FITAL_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $CLAVE" \ -H "X-Fital-Timestamp: $TS" \ -H "X-Fital-Signature: v1=$FIRMA" \ -d "$CUERPO" ``` ```js import { createHash, createHmac } from 'node:crypto'; const ref = 'mi-primer-pago'; const cuerpo = JSON.stringify({ beneficiary_id: process.env.BENEFICIARIO, amount: '100.00', currency: 'MXN', external_ref: ref }); const clave = `pago-${ref}-${process.env.BENEFICIARIO}`; const ts = String(Math.floor(Date.now() / 1000)); const hash = createHash('sha256').update(cuerpo).digest('hex'); const firma = createHmac('sha256', process.env.FITAL_SIGNING_SECRET) .update([ts, 'POST', '/v1/payments', hash, clave].join('\n')).digest('hex'); const r = await fetch(`${process.env.FITAL_API}/payments`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.FITAL_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': clave, 'X-Fital-Timestamp': ts, 'X-Fital-Signature': `v1=${firma}`, }, body: cuerpo, }); console.log(r.status, await r.json()); ``` ```python import hashlib, hmac, json, os, time, urllib.error, urllib.request ref = "mi-primer-pago" datos = {"beneficiary_id": os.environ["BENEFICIARIO"], "amount": "100.00", "currency": "MXN", "external_ref": ref} cuerpo = json.dumps(datos).encode() clave = "pago-" + ref + "-" + os.environ["BENEFICIARIO"] ts = str(int(time.time())) canon = "\n".join([ts, "POST", "/v1/payments", hashlib.sha256(cuerpo).hexdigest(), clave]) firma = hmac.new(os.environ["FITAL_SIGNING_SECRET"].encode(), canon.encode(), hashlib.sha256).hexdigest() pet = urllib.request.Request(os.environ["FITAL_API"] + "/payments", method="POST", data=cuerpo, headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"], "Content-Type": "application/json", "Idempotency-Key": clave, "X-Fital-Timestamp": ts, "X-Fital-Signature": "v1=" + firma}) try: with urllib.request.urlopen(pet) as r: print(r.status, json.load(r)) except urllib.error.HTTPError as e: print(e.code, e.read().decode()) ``` ```php getenv('BENEFICIARIO'), 'amount' => '100.00', 'currency' => 'MXN', 'external_ref' => $ref]); $clave = 'pago-' . $ref . '-' . getenv('BENEFICIARIO'); $ts = (string) time(); $canon = implode("\n", [$ts, 'POST', '/v1/payments', hash('sha256', $cuerpo), $clave]); $firma = hash_hmac('sha256', $canon, getenv('FITAL_SIGNING_SECRET')); $c = curl_init(getenv('FITAL_API') . '/payments'); curl_setopt_array($c, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('FITAL_KEY'), 'Content-Type: application/json', 'Idempotency-Key: ' . $clave, 'X-Fital-Timestamp: ' . $ts, 'X-Fital-Signature: v1=' . $firma, ], CURLOPT_POSTFIELDS => $cuerpo, ]); echo curl_exec($c), "\n"; ``` Responde `202` con `status: processing` y un `id` con prefijo `pay_`. La clave de idempotencia sale de tu referencia y del beneficiario: si corres esto otra vez, recibes el mismo pago, con `Idempotent-Replayed: true`, y no uno nuevo. Para crear otro pago, cambia la referencia. Ver [Idempotencia](/docs/idempotencia/). Guarda el `id`: ```bash export PAGO="pay_..." ``` ## 6. Míralo terminar Espera unos 30 segundos y consúltalo: ```bash curl -sS "$FITAL_API/payments/$PAGO" \ -H "Authorization: Bearer $FITAL_KEY" ``` ```js const r = await fetch(`${process.env.FITAL_API}/payments/${process.env.PAGO}`, { headers: { Authorization: `Bearer ${process.env.FITAL_KEY}` }, }); console.log(r.status, await r.json()); ``` ```python import json, os, urllib.error, urllib.request pet = urllib.request.Request(os.environ["FITAL_API"] + "/payments/" + os.environ["PAGO"], headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"]}) try: with urllib.request.urlopen(pet) as r: print(r.status, json.load(r)) except urllib.error.HTTPError as e: print(e.code, e.read().decode()) ``` ```php true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('FITAL_KEY')], ]); echo curl_exec($c), "\n"; ``` `status` llega en `completed`. Si sigue en `processing`, espera unos segundos y consulta otra vez, o llévalo a `completed` con el paso 7. Si tienes un destino de webhook, también recibes `payment.completed`. ## 7. Fuerza otro desenlace Sin esperar su guion, lleva un pago a otro estado con [`POST /sandbox/v1/payments/{id}/advance`](/docs/operaciones/avanzar-pago-de-prueba/). O provoca un error al crear el pago con `X-Fital-Scenario`. La lista está en [Ambientes](/docs/ambientes/#escenarios). ## Errores comunes - `401 wrong_environment`: usas una llave de otro ambiente. Aquí va tu llave `sk_test_fital_…`, no la llave pública del simulador. - `401 invalid_signature`: firmaste la ruta con `/sandbox`. Se firma siempre `/v1/...`. - `403 forbidden_scope`: a tu llave le falta un scope. `details.required_scope` lo nombra. - `409 payment_in_flight`: otro pago de tu empresa sigue sin confirmarse. Termínalo con el paso 7 o espera a que se resuelva. - `404 not_found`: el `id` no es de un beneficiario de tu cuenta. - `422 invalid_beneficiary`: el beneficiario se dio de baja. ## Qué sigue [Cuenta de prueba](/docs/sandbox/cuenta-de-prueba/) e [Inicio rápido de webhooks](/docs/webhooks/inicio-rapido/). --- # Conceptos Una empresa, una cuenta FITAL, y lo que se mueve dentro de ella. Tu empresa tiene una cuenta FITAL. Sobre ella hay saldos, beneficiarios, cotizaciones, pagos, cobros, depósitos y movimientos. Cada recurso tiene un identificador con prefijo. | Concepto | Prefijo | Qué es | |---|---|---| | Beneficiario | `ben_` | Destino de un pago. Se aprueba en el panel antes de poder pagarle | | Cotización | `quo_` | Monto de débito indicativo, con vencimiento | | Pago | `pay_` | Salida de dinero a un beneficiario | | Cobro | `col_` | Instrucción para que un cliente te pague | | Dirección de depósito | `dep_` | Dirección para recibir un token por una red | | Depósito | `dpt_` | Dinero recibido, con los datos del pagador si el riel los informa | | Movimiento | `mov_` | Cualquier entrada o salida de tu cuenta | ## Saldos y cuentas para recibir [`GET /v1/balances`](/docs/operaciones/listar-saldos/) da lo disponible por moneda. [`GET /v1/wallets`](/docs/operaciones/listar-wallets/) da lo mismo por wallet, con su red: la suma por moneda coincide, salvo redondeo al centavo, y las wallets sólo se consultan. [`GET /v1/receiving-accounts`](/docs/operaciones/listar-cuentas-para-recibir/) da las cuentas donde puedes recibir fondos, y [`POST /v1/receiving-accounts`](/docs/operaciones/abrir-cuenta-para-recibir/) abre una nueva, como en [Cuentas para recibir](/docs/recetas/cuentas-para-recibir/). [`GET /v1/limits`](/docs/operaciones/consultar-limites/) da los rieles habilitados y el máximo aplicable, si existe. ## Beneficiarios El alta por API crea el beneficiario pendiente. La aprobación ocurre en el panel y nunca por API: una llave robada no puede pagar a un destino nuevo. No hay espera ni tope de monto; sí hay aprobación. | Estado | Significado | |---|---| | `processing` | El alta sigue en curso | | `pending_approval` | Falta aprobarlo en el panel | | `approved` | Ya se le puede pagar | | `rejected` | No se aprobó | | `deleted` | Se dio de baja | ## Pagos El riel sale del beneficiario, no lo eliges en la petición. Queda fijo al crear el pago: dar de baja al beneficiario después no lo cambia. Estos son los estados públicos: | Estado | Significado | ¿Terminal? | |---|---|---| | `processing` | Aceptado y en curso, o aún sin confirmar | No | | `in_review` | En revisión. **No repitas el pago** | No | | `completed` | Se pagó. Aún puede pasar a `returned` | Sí | | `failed` | No se hizo y no queda dinero en tránsito | Sí | | `returned` | Se devolvió y el saldo ya volvió | Sí | | `canceled` | Se canceló antes de ejecutarse | Sí | Trata estas listas como extensibles: un valor nuevo no rompe la versión. Tu código debe tolerar un estado que no conoce. Con `failed`, [`GET /v1/payments/{id}`](/docs/operaciones/consultar-pago/) y `payment.failed` traen `failure_code`, un código del [catálogo de errores](/docs/errores/). Nunca llega en `null` en un pago `failed`: sin un motivo más específico, o si el detalle ya no se puede leer, es `payment_rejected`. La lista [`GET /v1/payments`](/docs/operaciones/listar-pagos/) no lo trae: ahí siempre es `null`. Los valores están en [Consultar un pago](/docs/operaciones/consultar-pago/#valores-de-failure_code). `processing` tampoco garantiza que el pago siga avanzando. Si un pago se reenvía y ese reenvío cae, sigue en `processing` hasta que FITAL lo cierre, y entonces llega su estado final. No lo repitas. Si lleva demasiado tiempo así, escribe a [soporte](/docs/soporte/) con el `pay_`. ### Un estado terminal se puede corregir Terminal significa que no esperas otro cambio, no que sea imposible. Una lectura parcial no mueve el estado de un pago, salvo un rechazo ya cerrado. Una lectura concluyente sí, también hacia atrás: un `completed` que se devuelve o un `canceled` que se reabre cambian de estado y publican su evento. Ver [Orden](/docs/webhooks/eventos/#orden). Esto aplica a pagos, no a cobros. ### Qué confirma un `completed` `completed` exige confirmación del riel. En `latam` la da el banco de destino: mientras no llegue, el pago sigue en `processing`. Si no se sabe el riel de un pago, ninguna lectura nueva lo da por `completed`: el detalle responde lo último que se sabe, con `freshness.status` en `delayed`, y su movimiento sale en `processing` con `read_status: "partial"`. ### Frescura del detalle [`GET /v1/payments/{id}`](/docs/operaciones/consultar-pago/) lee el estado en vivo y dice de dónde salió en `freshness`. Ese campo sólo existe en el detalle. | `freshness.status` | Qué significa | `freshness.as_of` | |---|---|---| | `current` | `status` sale de una lectura de ahora | La hora de esa lectura | | `delayed` | La lectura no concluyó: `status` es lo último que se sabe | La hora de la última lectura aplicada o de la cancelación confirmada. `null` si aún no hay ninguna | Con `delayed`, no tomes `status` como un resultado nuevo: consulta otra vez más tarde. Una cancelación confirmada responde `canceled` con `delayed` hasta que una lectura la confirme. Si ese pago responde después `processing` con `current`, se reabrió de verdad. ### Lista y detalle [`GET /v1/payments`](/docs/operaciones/listar-pagos/) es una vista guardada: no lee en vivo y no lleva `freshness`. Puede ir una vuelta de sincronización por detrás del detalle, también justo después de cancelar, y se pone al día sola. Esa vuelta es la misma que publica los eventos: normalmente tarda de 20 a 120 segundos. Para decidir sobre un pago, usa el detalle. ### Vigilancia de un pago completado Un pago `completed` en `latam` se vigila leyendo el riel durante 120 días desde la primera lectura que lo vio completado. Después, una devolución tardía puede no generar evento ni aparecer en [`GET /v1/payments`](/docs/operaciones/listar-pagos/). Ese plazo no garantiza que una devolución no llegue más tarde. En los demás rieles nada lee el riel por su cuenta: la devolución se conoce por el aviso del riel. Es el caso normal, y entonces llega `payment.returned`. Si ese aviso no llegara y nadie consultara el pago, la devolución no se vería, ni dentro de esos 120 días ni después. En cualquier riel, [`GET /v1/payments/{id}`](/docs/operaciones/consultar-pago/) lee en vivo y da el estado real. Si tu cierre depende de un pago, consúltalo ahí. Si esa consulta ve otro estado, el pago vuelve a vigilarse y el cambio se publica. ## Cotizaciones [`POST /v1/quotes`](/docs/operaciones/crear-cotizacion/) devuelve cuánto se debitaría, en `source_amount` y `source_currency`. Es indicativa y vence en `expires_at`, a los 60 segundos en producción. Si envías su `quote_id` al crear el pago, el débito no puede superar lo cotizado. ## Errores comunes - Dar por fallido un pago en `processing`. Consulta el `pay_` hasta un estado terminal. - Tomar `delayed` como un resultado nuevo. Es lo último que se sabe. - Decidir sobre un pago con [`GET /v1/payments`](/docs/operaciones/listar-pagos/). Es una vista: usa el detalle. - Pagar a un beneficiario sin aprobar: responde `422 beneficiary_not_approved`. ## Qué sigue [Ambientes](/docs/ambientes/) y [Autenticación](/docs/autenticacion/). --- # Cobertura Países, monedas y rieles disponibles, y lo que puedes hacer en cada uno. Esta página dice dónde opera la API. Lo que tu empresa tiene habilitado hoy está en [`GET /v1/limits`](/docs/operaciones/consultar-limites/), en `rails`. ## Pagos por riel El riel sale del beneficiario. Cada riel paga en su moneda: otra responde `422 currency_not_supported`. | Riel | País | Moneda | Datos de la cuenta | Guía | |---|---|---|---|---| | `mxn_spei` | México | `MXN` | CLABE | [Pago SPEI](/docs/recetas/pago-spei/) | | `usd_ach` | Estados Unidos | `USD` | Número de ruta, cuenta y domicilio | [Pago ACH](/docs/recetas/pago-ach/) | | `usd_wire` | Estados Unidos | `USD` | Número de ruta, cuenta y domicilio | [Pago wire](/docs/recetas/pago-wire/) | | `eur_sepa` | Zona euro | `EUR` | IBAN y BIC | [Pago SEPA](/docs/recetas/pago-sepa/) | | `gbp_fps` | Reino Unido | `GBP` | Número de cuenta y sort code | [Pago FPS](/docs/recetas/pago-fps/) | | `cop` | Colombia | `COP` | Llave Bre-B | [Pago Bre-B](/docs/recetas/pago-bre-b/) | | `pix` | Brasil | `BRL` | Llave PIX | [Pago PIX](/docs/recetas/pago-pix/) | | `latam` | Ver [Países del riel `latam`](/docs/cobertura/#países-del-riel-latam) | La del país | Cuenta local, según el país | [Pago LatAm](/docs/recetas/pago-latam/) | Un riel habilitado puede no estar disponible por un momento: esa respuesta es `422 rail_unavailable` y se repite más tarde. Las capacidades activas para tu empresa están en [`GET /v1/status`](/docs/operaciones/consultar-estado/). ## Qué puedes hacer en cada riel | Operación | Rieles | |---|---| | Registrar beneficiarios y pagar | Todos | | Cotizar antes de pagar | Todos | | Cancelar un pago que aún espera fondos | Todos, salvo `latam` | | Cobrar | Según el método: ver [Cobros](#cobros) | ## Países del riel `latam` | País | `country` | `currency` | `account_number` | `entity` | `account_type` | `document_type` | |---|---|---|---|---|---|---| | Argentina | `AR` | `ARS` | CBU o CVU, 22 dígitos | No | No | `DNI`, `CUIL`, `CUIT` | | Bolivia | `BO` | `BOB` | De 5 a 20 dígitos | Obligatorio | Opcional | `CED_CIU`, `NIT` | | Chile | `CL` | `CLP` | De 5 a 20 dígitos | Obligatorio | Opcional | `RUT`, `PP` | | Perú | `PE` | `PEN` | CCI, 20 dígitos | No | Obligatorio | `DNI`, `RUC`, `CED_EXT` | La tabla orienta. [`GET /v1/latam/countries`](/docs/operaciones/listar-paises-latam/) manda: sólo trae los países de tu empresa, con la configuración vigente, y `available` dice si alguno está restringido por ahora. Los bancos de cada país están en [`GET /v1/latam/countries/{country}/entities`](/docs/operaciones/listar-bancos-latam/), y el alta paso a paso en [Pago LatAm](/docs/recetas/pago-latam/). ## Cobros Los métodos dependen de tu empresa y cambian con el tiempo. Por eso no se listan aquí: [`GET /v1/collection-methods`](/docs/operaciones/listar-metodos-de-cobro/) da cada método con su país, moneda, mínimo, máximo y lo que pide de quien paga. Ver [Cobros](/docs/recetas/cobros/). ## Redes y tokens Una dirección de depósito recibe un solo token por una sola red: | `chain` | `currency` | Devolución automática | |---|---|---| | `arbitrum` | `USDC` | Sí | | `avalanche` | `USDC` | Sí | | `base` | `EURC` | Sí | | `base` | `USDC` | Sí | | `celo` | `USDC` | Sí | | `ethereum` | `EURC` | Sí | | `ethereum` | `PYUSD` | Sí | | `ethereum` | `USDC` | Sí | | `ethereum` | `USDT` | Sí | | `linea` | `USDC` | No | | `optimism` | `USDC` | Sí | | `polygon` | `USDC` | Sí | | `polygon` | `USDT` | Sí | | `solana` | `EURC` | No | | `solana` | `PYUSD` | No | | `solana` | `USDC` | No | | `solana` | `USDT` | No | | `tempo` | `USDC` | No | | `tempo` | `USDT` | No | | `tron` | `USDT` | No | | `xdc` | `USDC` | No | Otra combinación responde `422 pair_not_supported`. Al pedirla, `chain` y `currency` no distinguen mayúsculas; la respuesta trae `chain` en minúsculas y `currency` en mayúsculas. USDT por `polygon` puede responder `422 pair_not_supported` mientras no esté activo. En una red sin devolución automática, un depósito rechazado no regresa solo. Ver [Direcciones de depósito](/docs/recetas/direcciones-deposito/). ## Cuenta de prueba Sólo simula SPEI en MXN. Otro riel responde `403 rail_not_enabled`, y [`GET /sandbox/v1/latam/countries`](/docs/operaciones/listar-paises-latam/) da países ficticios con `available: false`. Ver [Qué se simula](/docs/sandbox/cuenta-de-prueba/#qué-se-simula). ## Errores comunes - Dar por hecho que un riel de esta lista está en tu empresa. Consulta [`GET /v1/limits`](/docs/operaciones/consultar-limites/). - Enviar decimales en una moneda que no los lleva. Ver [Montos y fechas](/docs/montos-y-fechas/). ## Qué sigue [Pago SPEI](/docs/recetas/pago-spei/) y [Pago LatAm](/docs/recetas/pago-latam/). --- # Ambientes Simulador público, cuenta de prueba y producción, y cómo pasar de uno a otro. Hay tres ambientes con el mismo contrato. Cambiar de ambiente es cambiar la base y la llave, sin tocar tu código. | | Simulador público | Cuenta de prueba | Producción | |---|---|---|---| | Base | `https://widget.fital.xyz/explorar/v1` | `https://widget.fital.xyz/sandbox/v1` | `https://widget.fital.xyz/v1` | | Cuenta | No hace falta | Panel y KYB aprobado, automático | Panel, KYB aprobado e inscripción de FITAL | | Llave | Pública de prueba | `sk_test_fital_…`, por empresa | `sk_live_fital_…`, con login reciente | | Dinero | Nunca | Nunca; saldos ficticios | Real | | Estado entre llamadas | No guarda | Sí, por empresa | Real | | Idempotencia | No real | Real | Real | | Webhooks | [Script local](/docs/webhooks/inicio-rapido/) | Simulados y firmados | Reales | | IP permitidas | No aplica | Opcional | **Obligatoria** | | Firma de webhook | [Vector público](/docs/webhooks/verificar-firma/#vector-de-prueba) | Obligatoria | Obligatoria | | `X-Fital-Scenario` | Sí | Sí | No aplica | | Reinicio de datos | No hace falta | Sí, desde el panel | **No existe** | | Rieles | Todos, sólo la forma | SPEI con CLABE `999`; otro riel da `403 rail_not_enabled` | Los habilitados a tu empresa | | Límite de peticiones | 10 por segundo por IP | 5 por segundo por llave | Ver [Límites](/docs/limites/) | | Tiempos | Inmediatos | Acortados | Reales de cada banco | | Comisiones y tipos de cambio | Ejemplo | Ficticios | Reales | | Revisiones y cumplimiento | No | Simulados por escenario, con tiempos ficticios | Reales; su duración no es predecible | La cuenta de prueba es automática con KYB aprobado. Producción requiere además la inscripción de FITAL: sin ella, el panel responde `403 live_not_enabled` al crear una llave `sk_live_fital_…`. Las respuestas de ambos simuladores llevan `X-Fital-Environment: sandbox`, y las de la cuenta de prueba, `livemode: false` en su JSON. ## Qué no simula el simulador público Estado entre llamadas, idempotencia real, saldos, tiempos, límites reales, comisiones, tipos de cambio, firmas ni KYB. Prueba la forma de tu integración, no su conducta. Para eso existe la cuenta de prueba. Tres diferencias concretas. Una petición que no cumple el esquema, también si falta `Idempotency-Key` o es muy corta, responde `422` con `invalid_request` y sin señalar un campo, cuando la API real responde `400` con el detalle. Un escenario que no aplica a la operación responde `404` en formato `application/problem+json`. Y `request_id` del cuerpo de un error es fijo: el valor de cada petición viene en el encabezado `X-Request-Id`. Tu código de producción debe esperar siempre la forma de [Errores](/docs/errores/). ## Escenarios Envía `X-Fital-Scenario: ` para elegir la respuesta. | Escenario | Respuesta | Termina en | Enseña a manejar | |---|---|---|---| | `payment_completed` | 202 `processing` | Pago `completed` | Termina en `completed`. | | `payment_failed` | 202 `processing` | Pago `failed` | El pago no se hizo y no queda dinero en tránsito. Si no ves `failed`, no repitas. | | `payment_returned` | 202 `processing` | Pago `returned` | Devolución después de `completed`. | | `insufficient_funds` | 422 `insufficient_funds` | No aplica | Saldo insuficiente. | | `invalid_beneficiary` | 422 `invalid_beneficiary` | No aplica | Cuenta destino inválida. | | `idempotency_key_reused` | 422 `idempotency_key_reused` | No aplica | Misma clave con otro cuerpo. | | `pix_not_enabled` | 403 `not_enabled` | No aplica | PIX sin activación vigente para tu empresa. | | `invalid_request` | 400 `invalid_request` | No aplica | Errores por campo en `details.fields`. | | `invalid_api_key` | 401 `invalid_api_key` | No aplica | Llave mala o revocada. | | `forbidden_scope` | 403 `forbidden_scope` | No aplica | Falta un scope. | | `kyb_required` | 403 `kyb_required` | No aplica | Empresa sin aprobación. | | `ip_not_allowed` | 403 `ip_not_allowed` | No aplica | IP fuera de la lista de la llave. | | `not_found` | 404 `not_found` | No aplica | Recurso ajeno o inexistente. | | `rate_limited` | 429 `rate_limited` | No aplica | Esperar lo que indique `Retry-After`. | | `service_unavailable` | 503 `service_unavailable` | No aplica | Reintento con espera. | | `status_degraded` | 200 | No aplica | Pausar sólo lo afectado. | **Respuesta** es lo que devuelve la petición, y **Termina en** es el estado final del recurso. `No aplica` significa que la respuesta ya es el resultado. Un pago siempre nace en `202 processing`: el final no llega en esa respuesta. En el simulador público lo ves al consultar [`GET /v1/payments/{id}`](/docs/operaciones/consultar-pago/) con el mismo escenario. En la cuenta de prueba, el pago avanza solo hasta ahí y te llega por webhook. Sin encabezado obtienes el camino feliz: `2xx`, y en un pago, `processing`. Estos escenarios existen sólo en la cuenta de prueba: | Escenario | Respuesta | Termina en | Enseña a manejar | |---|---|---|---| | `payment_in_review` | 202 `processing` | Pago `completed` | Revisión que termina en `completed`. | | `payment_in_review_failed` | 202 `processing` | Pago `failed` | Revisión que termina en `failed`. El tiempo real de una revisión no es predecible. | | `payment_held` | 422 `in_review` | No aplica | Sin `pay_`. Repite con la misma clave tras levantarse: crea un pago. | | `payment_uncertain` | 202 `processing` | Pago `completed` | Resultado sin confirmar: la misma clave repite la respuesta y otra clave recibe `409 payment_in_flight`. | | `account_restricted` | 403 `account_restricted` | No aplica | Cuenta restringida, sin `pay_`. | | `collection_paid` | 201 `pending` | Cobro `paid` | Cobro que se paga, con su evento firmado. | | `collection_expired` | 201 `pending` | Cobro `expired` | Cobro que vence, con su evento firmado. | | `receiving_account_not_enabled` | 403 `not_enabled` | No aplica | Moneda sin activación para tu empresa. No abre nada. | | `receiving_account_processing` | 202 `processing` | undefined `active` | Alta sin confirmar: no devuelve datos. Repite sin el escenario con la misma clave y se abre. | | `deposit_received` | 201 `active` | Depósito `received` | Depósito con pagador ficticio, con su evento firmado. | `collection_paid` y `collection_expired` se envían al crear el cobro, y `deposit_received` al crear la dirección de depósito. Lee el cobro con [`GET /sandbox/v1/collections/{id}`](/docs/operaciones/consultar-cobro/) y el depósito con [`GET /sandbox/v1/deposits`](/docs/operaciones/listar-depositos/); también llegan por webhook. ## Errores comunes - `401 wrong_environment`: la llave no es de esa base. En el simulador público, `sk_test_fital_…` también recibe este error; su base es `https://widget.fital.xyz/sandbox/v1`. - Confiar en un `200` del simulador público como prueba de que tu flujo funciona. Sólo prueba la forma. ## Qué sigue [Cuenta de prueba](/docs/sandbox/cuenta-de-prueba/) y [Lista para producción](/docs/produccion/). --- # Cuenta de prueba Tu ambiente propio con datos ficticios, escenarios y webhooks simulados. La cuenta de prueba es un ambiente por empresa con el mismo contrato que producción: guarda estado, aplica idempotencia real y entrega webhooks firmados. El dinero y los bancos no existen ahí. Toda respuesta JSON lleva `livemode: false`. Para empezar, sigue el [inicio rápido](/docs/sandbox/inicio-rapido/). ## Crear la llave de prueba El panel está en [merchants.fital.xyz](https://merchants.fital.xyz): entran las empresas con KYB aprobado en [empresa.fital.xyz](https://empresa.fital.xyz). Si tu empresa aún no tiene cuenta, regístrala ahí y completa el KYB. Con KYB aprobado, entras y creas tu llave `sk_test_fital_…`. Es automático. Sin KYB aprobado la respuesta es `403 kyb_required`, sin vía manual. La llave y su secreto de firma se muestran una sola vez, y la llave caduca a los 90 días. Al revocar una llave de prueba, las escrituras se rechazan al instante; una lectura puede seguir pasando hasta 30 segundos. ## Hacer un pago ficticio No hay botón de pago en el panel. Usa las muestras del [inicio rápido](/docs/sandbox/inicio-rapido/), o el script para Node 22 de la pantalla Sandbox, que ya trae el beneficiario de prueba. Así pruebas también la firma de la petición. En unos 30 segundos el pago termina y llega `payment.completed` a tu destino. Lo ves en las pantallas Webhooks y Registros de API. ## Qué se simula Sólo SPEI en MXN: - **Beneficiarios:** `rail` debe ser `mxn_spei`, la CLABE debe empezar con `999` y el titular debe incluir la palabra `FICTICIO`. Otro riel responde `403 rail_not_enabled`. Una CLABE o un titular reales responden `422 sandbox_real_account_rejected`. - **Cobros:** con el método `spei`, que pide `firstName`, `lastName` y `email` en `payer`, igual que dice `payer_fields`, y con `efectivo`, que no pide nada de quien paga y responde una referencia ficticia. Sin esos campos, `spei` responde `400 invalid_request`. Otro método responde `422 method_unavailable`. La cuenta de prueba simula `efectivo` aunque tu empresa aún no lo haya pedido a FITAL para producción. - **Catálogo `latam`:** [`GET /sandbox/v1/latam/countries`](/docs/operaciones/listar-paises-latam/) responde países ficticios con `available: false`, y [`GET /sandbox/v1/latam/countries/{country}/entities`](/docs/operaciones/listar-bancos-latam/) un banco ficticio. Sirven para leer la forma de la respuesta: el alta `latam` no procede aquí. - **Saldos:** ficticios, en MXN y USD. El saldo USD sólo se ve: un pago o una cotización en otra moneda que no sea MXN responde `422 currency_not_supported`. - **Wallets:** dos ficticias, en `base` y `solana`. Salen del mismo saldo ficticio: `MXN` no vive en una wallet. - **Cuentas para recibir:** [`POST /sandbox/v1/receiving-accounts`](/docs/operaciones/abrir-cuenta-para-recibir/) abre cuentas ficticias que no reciben dinero. Abre en `USD`, `EUR`, `BRL`, `GBP` y `COP` sin pedir activación. - **Direcciones de depósito:** son ficticias y no reciben dinero. - **Cotizaciones:** con tipo de cambio ficticio. Si el débito supera `max_source_amount`, el pago responde `409 quote_changed`, sin efecto, igual que en producción. ## Datos ficticios incluidos La cuenta nace con saldo ficticio y beneficiarios ficticios, cada uno con un escenario asignado; los ves en la pantalla Sandbox. Ningún dato se copia de producción. ## Escenarios `X-Fital-Scenario` manda sobre el escenario del beneficiario. Sin ninguno de los dos, el pago sigue el camino feliz. Ver la lista en [Ambientes](/docs/ambientes/#escenarios). | Caso | Cuánto tarda | |---|---| | Pago que termina, falla o se devuelve | Unos 30 segundos hasta su primer resultado; una devolución llega 2 minutos después | | Pago en revisión | 2 minutos | | Pago sin confirmar (`payment_uncertain`) | 5 minutos. Mientras tanto, otro pago de tu empresa responde `409 payment_in_flight`. Con `advance` lo terminas antes | | Pago retenido (`payment_held`) | 20 segundos antes de poder repetir con la misma clave | | Cobro pagado o vencido, y depósito recibido | 30 segundos | Estos tiempos no anticipan los de producción. ## Avanzar un pago [`POST /sandbox/v1/payments/{id}/advance`](/docs/operaciones/avanzar-pago-de-prueba/) lleva un pago a `completed`, `failed`, `returned` o `in_review` sin esperar, y emite su evento firmado. Exige `Idempotency-Key` y firma. Sigue las reglas de un pago real: una transición imposible responde `409 sandbox_invalid_transition`. Esta ruta no existe en producción. ## Webhooks simulados [`POST /sandbox/v1/webhooks/trigger`](/docs/operaciones/emitir-evento-de-prueba/) emite un evento firmado a tus destinos de prueba, con los mismos encabezados y el mismo calendario de reintentos que producción. `data` tiene la forma real de su tipo, con datos ficticios, más `simulated: true` y `livemode: false`. Los destinos se dan de alta en el panel y sólo aceptan HTTPS público. Esta ruta tampoco existe en producción. El contrato de este ambiente, con estas dos rutas, está en [`openapi.sandbox.json`](/docs/openapi.sandbox.json). Para probar en tu máquina usa el [inicio rápido de webhooks](/docs/webhooks/inicio-rapido/). Un pago de prueba no se reabre. Para practicar ese retroceso, emite `payment.canceled` y después `payment.processing` con el mismo `resource_id`: `sequence` crece por recurso. Cada destino recibe hasta 2 entregas simultáneas, y la cola guarda hasta 500 pendientes por destino y 1,500 por empresa. Lo que no cabe queda `skipped`, sin entrega: el evento sigue en [`GET /sandbox/v1/events`](/docs/operaciones/listar-eventos/). [`GET /sandbox/v1/meta/egress-ips`](/docs/operaciones/consultar-ip-de-salida/) responde una lista vacía: este ambiente no publica IP de salida. La [lista de producción](/docs/seguridad/ip-webhooks/) no aplica aquí. ## Reinicio de datos Desde el panel. Borra los datos de tu empresa y restaura la semilla; conserva llaves y destinos. Hasta 5 veces al día. ## Límites | Límite | Valor | |---|---| | Peticiones | 5 por segundo por llave, ráfaga de 10; lecturas y escrituras igual | | Por IP | 10 por segundo, ráfaga de 20 | | Pagos y cobros | 100 al día; después, `429 sandbox_limit` | | Beneficiarios | 20 | | Destinos de webhook | 3 | | Monto por pago | 1,000,000 ficticios; más responde `422 limit_exceeded` | | Eventos guardados | 10,000 por empresa: los más viejos se borran, sin error | | Datos sin actividad | Se borran a los 30 días y el siguiente uso restaura la semilla. Llaves y destinos no se tocan | ## Salud [`GET /sandbox/v1/health`](/docs/operaciones/consultar-salud/) mide sólo este ambiente, no producción. Ver [Salud y estado](/docs/salud-y-estado/). ## Qué no prueba este ambiente Tiempos reales de cada banco, comisiones y tipos de cambio reales, otros rieles, ni la duración de una revisión de cumplimiento. Aquí una `Idempotency-Key` vencida se borra: pasados 7 días, otro cuerpo con esa clave ya no responde `422 idempotency_key_reused`, como sí en producción. Tampoco prueba una lectura de producción: un pago en curso siempre responde `freshness.status` en `current`. ## Errores comunes - `401 wrong_environment`: usar aquí una llave de otro ambiente, como `sk_live_fital_…` o la llave pública del simulador. - `sandbox_real_account_rejected`, `sandbox_limit` y `sandbox_invalid_transition`: ver [Errores](/docs/errores/). - `401 invalid_signature`: firmar la ruta con el prefijo `/sandbox`. Se firma siempre `/v1/...`. Ver [Autenticación](/docs/autenticacion/#firma-de-petición). ## Qué sigue [Lista para producción](/docs/produccion/). --- # Autenticación Llaves, scopes, firma de petición e IP permitidas. Toda petición lleva tu llave en `Authorization: Bearer`. Las que mueven dinero llevan además una firma. En producción, la llave sólo funciona desde las IP que registraste. ## Llaves Se crean en la pantalla Llaves del panel. El formato es `sk_live_fital___` en producción y `sk_test_fital_…` en la cuenta de prueba. Al crear una llave ves también su **secreto de firma**, una sola vez. Guarda ambos en tu gestor de secretos. | | Producción | Cuenta de prueba | |---|---|---| | Quién la crea | Propietario o admin, con login de hace menos de 5 minutos | Propietario, admin o desarrollador | | Caduca a los | 180 días | 90 días | | Lista de IP | Obligatoria | Opcional | ### Vencimiento La pantalla Llaves muestra la fecha de cada llave. La pantalla Avisos te avisa 30, 7 y 1 días antes, sin correo. Una llave vencida responde `401 invalid_api_key` y ya no se puede rotar: tendrás que crear otra, con su lista de IP. ### Varias empresas Un usuario del panel puede pertenecer a más de una empresa. En ese caso el panel muestra el selector **Empresa**: cambiar de empresa abre una sesión nueva, y el login reciente no viaja. Llaves, listas de IP, destinos de webhook y datos son de cada empresa. Por API no hay selector: la llave determina la empresa. ## Scopes Cada llave tiene sólo los permisos que le des. La referencia indica el scope de cada operación. | Scope | Operaciones | |---|---| | `balances:read` | [`GET /v1/balances`](/docs/operaciones/listar-saldos/), [`GET /v1/limits`](/docs/operaciones/consultar-limites/) | | `beneficiaries:read` | [`GET /v1/beneficiaries`](/docs/operaciones/listar-beneficiarios/), [`GET /v1/beneficiaries/{id}`](/docs/operaciones/consultar-beneficiario/), [`GET /v1/latam/countries`](/docs/operaciones/listar-paises-latam/), [`GET /v1/latam/countries/{country}/entities`](/docs/operaciones/listar-bancos-latam/) | | `beneficiaries:write` | [`POST /v1/beneficiaries`](/docs/operaciones/crear-beneficiario/), [`DELETE /v1/beneficiaries/{id}`](/docs/operaciones/eliminar-beneficiario/) | | `collections:read` | [`GET /v1/collection-methods`](/docs/operaciones/listar-metodos-de-cobro/), [`GET /v1/collections`](/docs/operaciones/listar-cobros/), [`GET /v1/collections/{id}`](/docs/operaciones/consultar-cobro/) | | `collections:write` | [`POST /v1/collections`](/docs/operaciones/crear-cobro/), [`POST /v1/collections/estimate`](/docs/operaciones/estimar-cobro/) | | `deposit_addresses:read` | [`GET /v1/deposit-addresses`](/docs/operaciones/listar-direcciones-de-deposito/), [`GET /v1/deposit-addresses/{id}`](/docs/operaciones/consultar-direccion-de-deposito/) | | `deposit_addresses:write` | [`POST /v1/deposit-addresses`](/docs/operaciones/crear-direccion-de-deposito/) | | `deposits:read` | [`GET /v1/deposits`](/docs/operaciones/listar-depositos/), [`GET /v1/deposits/{id}`](/docs/operaciones/consultar-deposito/) | | `events:read` | [`GET /v1/events`](/docs/operaciones/listar-eventos/), [`GET /v1/events/{id}`](/docs/operaciones/consultar-evento/) | | `movements:read` | [`GET /v1/movements`](/docs/operaciones/listar-movimientos/), [`GET /v1/movements/{id}`](/docs/operaciones/consultar-movimiento/), [`GET /v1/movements/{id}/receipt`](/docs/operaciones/descargar-recibo/) | | `payments:read` | [`GET /v1/payments`](/docs/operaciones/listar-pagos/), [`GET /v1/payments/{id}`](/docs/operaciones/consultar-pago/) | | `payments:write` | [`POST /v1/payments`](/docs/operaciones/crear-pago/), [`POST /v1/payments/{id}/cancel`](/docs/operaciones/cancelar-pago/) | | `quotes:write` | [`POST /v1/quotes`](/docs/operaciones/crear-cotizacion/) | | `accounts:read` | [`GET /v1/receiving-accounts`](/docs/operaciones/listar-cuentas-para-recibir/) | | `accounts:write` | [`POST /v1/receiving-accounts`](/docs/operaciones/abrir-cuenta-para-recibir/) | | `wallets:read` | [`GET /v1/wallets`](/docs/operaciones/listar-wallets/) | | `webhooks:read` | [`GET /v1/webhook-endpoints`](/docs/operaciones/listar-destinos-de-webhook/) | [`GET /v1/status`](/docs/operaciones/consultar-estado/) acepta cualquier llave válida. [`GET /v1/health`](/docs/operaciones/consultar-salud/) y [`GET /v1/meta/egress-ips`](/docs/operaciones/consultar-ip-de-salida/) son públicas. ## Firma de petición Con los scopes `payments:write`, `beneficiaries:write`, `collections:write`, `deposit_addresses:write`, `accounts:write` el bearer solo no basta. Una llave robada sin el secreto de firma no saca dinero. ```text X-Fital-Timestamp: X-Fital-Signature: v1= ``` La firma es HMAC-SHA256, usando como clave el secreto de firma tal como lo recibiste, sobre estas cinco líneas unidas por salto de línea: ```text timestamp MÉTODO ruta y consulta, siempre desde /v1/ sha256 hexadecimal de los bytes del cuerpo Idempotency-Key, o vacío si la operación no la lleva ``` La ruta es siempre la canónica, `/v1/...`, también en los simuladores: cambiar de ambiente no cambia tu código. Una firma vale 5 minutos. La clave de idempotencia va firmada. ### Vector de prueba Comprueba tu implementación con este caso. El resultado debe coincidir exactamente. Todos sus datos son ficticios y están publicados a propósito: el secreto del vector no pertenece a ninguna llave y no sirve en ningún ambiente. ```bash CUERPO='{"beneficiary_id":"ben_demo","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"}' SECRETO_FICTICIO=c15dc9d93f7e23efa923836a3a9d2f7abc8bc9e6d6f353b5a827f7315f25dc9a HASH=$(printf '%s' "$CUERPO" | openssl dgst -sha256 -r | cut -d' ' -f1) FIRMA=$(printf '%s\n%s\n%s\n%s\n%s' 1793800000 POST /v1/payments "$HASH" pago-factura-1042 \ | openssl dgst -sha256 -hmac "$SECRETO_FICTICIO" -r | cut -d' ' -f1) echo "v1=$FIRMA" ``` ```js import { createHash, createHmac } from 'node:crypto'; const cuerpo = '{"beneficiary_id":"ben_demo","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"}'; const secreto_ficticio = 'c15dc9d93f7e23efa923836a3a9d2f7abc8bc9e6d6f353b5a827f7315f25dc9a'; const hash = createHash('sha256').update(cuerpo).digest('hex'); const canon = ['1793800000', 'POST', '/v1/payments', hash, 'pago-factura-1042'].join('\n'); console.log(`v1=${createHmac('sha256', secreto_ficticio).update(canon).digest('hex')}`); ``` ```python import hashlib, hmac cuerpo = b'{"beneficiary_id":"ben_demo","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"}' secreto_ficticio = b"c15dc9d93f7e23efa923836a3a9d2f7abc8bc9e6d6f353b5a827f7315f25dc9a" canon = "\n".join(["1793800000", "POST", "/v1/payments", hashlib.sha256(cuerpo).hexdigest(), "pago-factura-1042"]) print("v1=" + hmac.new(secreto_ficticio, canon.encode(), hashlib.sha256).hexdigest()) ``` ```php ` en [`POST /v1/collections`](/docs/operaciones/crear-cobro/) | No se creó nada, pero la clave conserva ese cuerpo | Una **nueva** para el cuerpo corregido | | `422 idempotency_key_reused` | La clave ya identifica otra operación | Corrige tu integración | ## El error `in_review` y el estado `in_review` son distintos - El **error** `422 in_review` con `retryable: true` responde a tu petición antes de ejecutarla y no hay `pay_`. Repite con la misma clave tras la revisión. - El mismo error con `retryable: false` quedó registrado al ejecutar: el pago queda `failed`, con `failure_code: in_review`. Esa clave repite el rechazo, así que usa una nueva tras la revisión. - El **estado** `in_review` es de un pago que ya existe. Ese pago sigue en curso: no lo vuelvas a crear, consúltalo. ## Qué significa cada resultado - `202 processing` no es fallo. El `pay_` se conserva y se consulta. - `failed`: el pago no se hizo y no queda dinero en tránsito. Para intentarlo de nuevo, usa una clave nueva. - `returned`: el saldo ya volvió. ## Errores comunes - Generar una clave nueva tras un `401` o `403`. Esos errores no dicen si el pago ocurrió: consulta el `pay_`. - Reutilizar una clave para otro pago: responde `422 idempotency_key_reused`. ## Qué sigue [Errores](/docs/errores/) y [Reintentos](/docs/recetas/reintentos/). --- # Errores Cada código, su causa, su solución y si se reintenta. Los errores de la API llegan con una sola forma, salvo las dos excepciones de abajo. Lee `code` para decidir, muestra `message` y guarda `request_id`. ## Forma del error ```json { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "amount": "Monto decimal inválido" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `details.fields` trae los errores por campo. `retryable` dice si repetir la misma petición más tarde puede servir, y es el dato de cada respuesta el que manda. Las tablas de abajo dan el valor habitual de cada código: un rechazo ya registrado siempre llega con `false`, se repite tal cual con esa clave y necesita una nueva. Ver [Qué hacer con la clave tras un rechazo](/docs/idempotencia/#qué-hacer-con-la-clave-tras-un-rechazo). Tu código debe tolerar códigos nuevos. ### Excepciones | Respuesta | Forma | Qué hacer | |---|---|---| | `503` de [`GET /v1/health`](/docs/operaciones/consultar-salud/) | `{"status": "down", "api_version": "v1"}`, sin `error` | Decide por `status`. Es el único error de la API sin esta forma | | `404` del simulador público, cuando un escenario no aplica a la operación | `application/problem+json` | Sólo ocurre ahí. Ver [Ambientes](/docs/ambientes/#qué-no-simula-el-simulador-público) | Un error fuera de la lista de arriba también puede ser de tu proxy o de tu red, no de FITAL: si no trae `error.code`, trátalo como un timeout. ## Códigos ### `idempotency_key_invalid` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 400 | `false` | No | **Causa.** La clave no mide entre 8 y 200 caracteres, o viene duplicada. **Solución.** Envía una sola cabecera con una clave de largo válido. ### `idempotency_key_required` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 400 | `false` | No | **Causa.** Falta `Idempotency-Key` en una operación que la exige. **Solución.** Envía una clave derivada de tu identificador interno. ### `invalid_request` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 400 | `false` | No | **Causa.** Algún campo no cumple el esquema, el cuerpo no es JSON válido o supera el tamaño máximo. El detalle por campo llega en `details.fields`. **Solución.** Corrige los campos indicados antes de repetir. Un cuerpo demasiado grande llega con HTTP 413 y este mismo código: ver [Límites](/docs/limites/#cuerpo). ### `invalid_api_key` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 401 | `false` | No | **Causa.** Llave mal formada, vencida o revocada, o empresa suspendida. **Solución.** Revisa la llave en tu gestor de secretos o crea otra en el panel. ### `invalid_signature` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 401 | `false` | No, hasta corregir la firma | **Causa.** Falta la firma de la petición, es inválida o tiene más de 5 minutos. **Solución.** Revisa la cadena firmada en [Autenticación](/docs/autenticacion/#firma-de-petición) y el reloj de tu servidor. ### `wrong_environment` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 401 | `false` | No | **Causa.** Llave de otro ambiente: `sk_test_` en producción, o `sk_live_` en un simulador. **Solución.** Usa la base y la llave del mismo ambiente. Ver [Ambientes](/docs/ambientes/). ### `account_not_active` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 403 | `false` | No | **Causa.** La cuenta no está activa. No dice si un pago previo ocurrió. **Solución.** Consulta el `pay_` o escribe a soporte. ### `account_restricted` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 403 | `false` | No | **Causa.** Tu cuenta tiene una restricción. **Solución.** Contacta a tu ejecutivo FITAL. No reintentes en bucle. ### `forbidden_scope` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 403 | `false` | No | **Causa.** La llave no tiene el scope de esta operación. **Solución.** Crea en el panel una llave con el scope necesario. ### `ip_not_allowed` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 403 | `false` | No, hasta registrar la IP | **Causa.** La IP de origen no está en la lista de esa llave. **Solución.** Regístrala en el panel. Ver [IP permitidas](/docs/seguridad/ip-permitidas/). ### `kyb_required` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 403 | `false` | No | **Causa.** La empresa no tiene KYB aprobado o aún no está inscrita. **Solución.** Completa el KYB en [empresa.fital.xyz](https://empresa.fital.xyz). Si ya está aprobado, pide la inscripción a FITAL. ### `live_not_enabled` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 403 | `false` | No, hasta la inscripción | **Causa.** Tu empresa aún no está habilitada para producción: tiene KYB aprobado pero falta la inscripción de FITAL. Lo responde el panel al crear una llave `sk_live_fital_…`; la cuenta de prueba sigue disponible. **Solución.** Completa la inscripción con FITAL. Ver [Ambientes](/docs/ambientes/). ### `not_enabled` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 403 | `false` | No | **Causa.** Función sin activar para tu empresa, como PIX sin activación vigente o un método de cobro. **Solución.** Consulta con tu ejecutivo FITAL. ### `rail_not_enabled` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 403 | `false` | No | **Causa.** El riel de pago no está habilitado para esta cuenta. En la cuenta de prueba, cualquier riel que no sea SPEI. **Solución.** Pide a FITAL habilitarlo o usa otro riel. En la cuenta de prueba usa `mxn_spei`. ### `not_found` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 404 | `false` | No | **Causa.** El recurso no existe o es de otra empresa. La respuesta es la misma en ambos casos. **Solución.** Revisa el identificador y el ambiente. ### `beneficiary_ref_conflict` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 409 | `false` | No | **Causa.** El identificador del alta choca con un beneficiario que ya existe. **Solución.** Consulta [`GET /v1/beneficiaries`](/docs/operaciones/listar-beneficiarios/) antes de repetir. ### `beneficiary_state_conflict` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 409 | `false` | No | **Causa.** El estado actual del beneficiario no permite esta operación. **Solución.** Consulta su estado con [`GET /v1/beneficiaries/{id}`](/docs/operaciones/consultar-beneficiario/). ### `collection_not_ready` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 409 | `false` | No | **Causa.** El cobro aún no está listo para esta operación. **Solución.** Consulta el cobro con [`GET /v1/collections/{id}`](/docs/operaciones/consultar-cobro/). ### `idempotency_in_progress` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 409 | `true` | Sí, misma clave | **Causa.** Una petición anterior con esa clave sigue en curso. **Solución.** Repite con la misma clave tras `Retry-After`. ### `not_cancellable` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 409 | `false` | No | **Causa.** El pago ya no se puede cancelar. Es la respuesta normal cuando ya avanzó. **Solución.** Sigue el pago con [`GET /v1/payments/{id}`](/docs/operaciones/consultar-pago/). ### `payment_in_flight` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 409 | `true` | Sí, misma clave, tras resolverse | **Causa.** Otra salida de dinero de tu empresa sigue en curso. **Solución.** Repite con la misma clave cuando se resuelva. ### `quote_changed` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 409 | `false` | Tras recotizar, con clave nueva | **Causa.** El débito superaría `max_source_amount` o lo cotizado. No se creó ningún pago. También aplica en la cuenta de prueba. **Solución.** Pide otra cotización y usa una clave nueva. ### `quote_expired` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 409 | `false` | Tras recotizar, con clave nueva | **Causa.** La cotización `quote_id` venció antes de crear el pago. **Solución.** Pide otra cotización y usa una clave nueva. ### `receipt_not_available` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 409 | `false` | Cuando el movimiento sea terminal | **Causa.** El movimiento aún no llega a un estado terminal, o no tiene recibo. **Solución.** Consulta el movimiento y pide el recibo cuando sea terminal. ### `receiving_account_in_progress` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 409 | `true` | Sí, con espera | **Causa.** Otra alta de esa moneda sigue en curso. **Solución.** Espera unos segundos y reintenta con la misma clave. ### `sandbox_invalid_transition` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 409 | `false` | No | **Causa.** Sólo cuenta de prueba: `advance` pidió un cambio que un pago real no puede hacer, como llevar a `returned` un pago que no está `completed`. **Solución.** Consulta el estado del pago y pide una transición válida. Ver [Avanzar un pago](/docs/sandbox/cuenta-de-prueba/#avanzar-un-pago). ### `amount_below_minimum` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 422 | `false` | Con otro monto, con clave nueva | **Causa.** El monto está por debajo del mínimo aplicable. **Solución.** Aumenta el monto y usa una clave nueva. ### `amount_out_of_range` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 422 | `false` | Con otro monto | **Causa.** El monto está fuera del rango del método. **Solución.** Consulta `min_amount` y `max_amount` en [`GET /v1/collection-methods`](/docs/operaciones/listar-metodos-de-cobro/). ### `beneficiary_not_approved` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 422 | `true` | Tras aprobar en el panel, misma clave | **Causa.** Beneficiario propio sin aprobar. Uno ajeno o inexistente da `not_found`. **Solución.** Apruébalo en el panel y repite con la misma clave. ### `collection_rejected` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 422 | `false` | No | **Causa.** El cobro fue rechazado. **Solución.** Revisa los datos y conserva el `request_id`. ### `currency_not_supported` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 422 | `false` | No | **Causa.** La moneda no aplica a este beneficiario o riel. **Solución.** Revisa la moneda de la operación. ### `deposit_address_rejected` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 422 | `false` | No | **Causa.** La dirección de depósito fue rechazada. **Solución.** Revisa `chain` y `currency`. ### `idempotency_key_reused` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 422 | `false` | No | **Causa.** La misma clave se usó con otro cuerpo, también si pasaron más de 7 días. Es un error de programación. **Solución.** Corrige tu integración: una clave identifica una sola operación. ### `in_review` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 422 | `true` | Según `retryable`: misma clave con `true`, clave nueva con `false` | **Causa.** Operación frenada por revisión. No confundir con el estado `in_review` de un pago que ya existe. Con `retryable: true` se frenó antes de ejecutar: no se creó nada y no hay `pay_`. Con `retryable: false` quedó registrada al ejecutar: el pago queda `failed`, con `failure_code: in_review`. **Solución.** Espera la revisión. Con `retryable: true`, repite con la misma clave. Con `retryable: false`, esa clave repite el rechazo: usa una nueva. ### `insufficient_funds` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 422 | `false` | Tras fondear, con clave nueva | **Causa.** Saldo insuficiente. **Solución.** Fondea la cuenta y repite con una clave nueva: la misma clave repite este rechazo. ### `invalid_beneficiary` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 422 | `false` | No | **Causa.** El destino es inválido. En un alta `latam`, los datos no cumplen lo que pide el país. **Solución.** Revisa los datos de la cuenta destino. En `latam`, compáralos con [`GET /v1/latam/countries`](/docs/operaciones/listar-paises-latam/). Usa una clave nueva: la misma repite el rechazo. ### `limit_exceeded` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 422 | `false` | Con otro monto o más tarde, con clave nueva | **Causa.** Se rebasó el tope acumulado de la empresa o un límite del riel: ver [Límites](/docs/limites/#montos). En la cuenta de prueba, un tope del ambiente: ver [Cuenta de prueba](/docs/sandbox/cuenta-de-prueba/#límites). **Solución.** Ajusta el monto o el momento. Usa una clave nueva: la misma repite este rechazo. ### `method_unavailable` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 422 | `false` | No | **Causa.** El método de cobro no está disponible. **Solución.** Consulta [`GET /v1/collection-methods`](/docs/operaciones/listar-metodos-de-cobro/). ### `pair_not_supported` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 422 | `false` | No | **Causa.** El par de monedas no está disponible. **Solución.** Elige otro par de monedas. ### `payment_rejected` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 422 | `false` | No con la misma clave | **Causa.** El pago fue rechazado sin un motivo más específico. **Solución.** Conserva el `request_id` y escribe a soporte si persiste. La misma clave repite este rechazo. ### `rail_unavailable` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 422 | `true` | Sí, misma clave, más tarde | **Causa.** El riel no está disponible por ahora. **Solución.** Repite más tarde con la misma clave. ### `receiving_account_rejected` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 422 | `false` | No | **Causa.** La cuenta para recibir no se pudo abrir. **Solución.** Contacta a tu ejecutivo FITAL y conserva el `request_id`. ### `sandbox_real_account_rejected` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 422 | `false` | Con datos ficticios | **Causa.** Sólo cuenta de prueba: la CLABE no empieza con `999` o el titular no incluye la palabra `FICTICIO`. **Solución.** Usa una CLABE `999` y un titular con `FICTICIO`. Ningún dato real entra a este ambiente. ### `rate_limited` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 429 | `true` | Sí, tras `Retry-After` | **Causa.** Superaste un límite de peticiones. **Solución.** Espera lo que indique `Retry-After`. ### `sandbox_limit` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 429 | `false` | Al día siguiente | **Causa.** Sólo cuenta de prueba: alcanzaste el tope diario de pagos y cobros, o de reinicios de datos. **Solución.** Repite al día siguiente. El tope de pagos y cobros se puede liberar reiniciando los datos desde el panel. ### `internal_error` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 500 | `true` | Sí, misma clave | **Causa.** Fallo nuestro. **Solución.** Repite con la misma clave. Si persiste, escribe a soporte con el `request_id`. ### `service_unavailable` | HTTP | `retryable` | ¿Reintentar? | |---|---|---| | 503 | `true` | Sí, misma clave | **Causa.** El servicio no está disponible por ahora. **Solución.** Repite con la misma clave y espera exponencial. ## Errores comunes - Decidir por el texto de `message`. Puede cambiar; `code` no. - Reintentar un error con `retryable: false` sin corregir la causa. ## Qué sigue [Reintentos](/docs/recetas/reintentos/). --- # Webhooks Recibe eventos firmados, verifícalos y recupera lo que falte. FITAL te avisa por HTTPS cuando algo cambia. Cada entrega es un `POST` firmado a tu destino: verifica la firma, responde `2xx` rápido y procesa después. Para verlo funcionar en tu máquina, sigue el [inicio rápido](/docs/webhooks/inicio-rapido/). > **Próximo.** En producción, las entregas se activan junto con la API. En la cuenta de prueba ya funcionan, firmadas y con el mismo calendario de reintentos: ver [Webhooks simulados](/docs/sandbox/cuenta-de-prueba/#webhooks-simulados). ## Requisitos del receptor | Requisito | Valor | |---|---| | Protocolo | HTTPS en el puerto 443, sin usuario ni contraseña en la URL | | Certificado | Vigente, emitido por una autoridad reconocida y válido para tu dominio. Uno autofirmado o vencido cuenta como fallo | | Dirección | Tu dominio debe resolver a direcciones IPv4 públicas. Las entregas salen sólo por IPv4 | | Conexión | Hasta 5 segundos para abrirla | | Respuesta | 10 segundos para que FITAL reciba tu código de estado | | Éxito | Cualquier `2xx`. El cuerpo de tu respuesta se ignora y FITAL cierra la conexión al recibir el código: no dependas de seguir escribiendo después | | Redirecciones | No se siguen: un `3xx` cuenta como fallo | La URL se valida al darla de alta y otra vez en cada entrega. ## Cómo llega una entrega Método `POST`, con `Content-Type: application/json` y estos encabezados: | Encabezado | Contenido | |---|---| | `X-Fital-Timestamp` | Segundos unix de este intento. Cambia en cada reintento | | `X-Fital-Signature` | Una o más firmas `v1=`, separadas por comas | | `X-Fital-Event-Id` | Identificador del evento. Sirve para depurar y no va firmado: para deduplicar usa el `id` del cuerpo | | `X-Fital-Delivery-Id` | Identificador de la entrega a este destino. Es el mismo en sus reintentos | El cuerpo es el evento: ```json { "id": "evt_5mPqW2xZr8TnC1vH7kLdS4bJ", "type": "payment.completed", "resource_id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "api_version": "2026-11-01", "created_at": "2026-11-04T18:01:30.000000Z", "sequence": 2, "data": { "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "status": "completed" } } ``` `data` cambia según `type`. El [catálogo de eventos](/docs/webhooks/eventos/) trae el cuerpo de cada uno. Verifica la firma antes de interpretar el JSON: ver [Verificar la firma](/docs/webhooks/verificar-firma/). ## Dar de alta un destino Por API sólo se consulta, con [`GET /v1/webhook-endpoints`](/docs/operaciones/listar-destinos-de-webhook/). El alta, la edición, la baja, la rotación del secreto y la reactivación se hacen en el panel, y `v1` no publica operaciones para eso: un destino decide a dónde sale tu información, y el panel exige sesión, rol y bitácora que una llave no da. En producción requiere rol de propietario o admin y login de hace menos de 5 minutos; en la cuenta de prueba, también rol de desarrollador. 1. **Crear.** En la pantalla Webhooks elige **Agregar webhook**. Escribe la URL y los eventos que quieres recibir; sin elegir ninguno, recibes todos. Cada URL se registra una sola vez por ambiente. 2. **Guardar el secreto.** El panel muestra el secreto de firma de ese destino una sola vez. Guárdalo en tu gestor de secretos. 3. **Verificar.** Implementa [Verificar la firma](/docs/webhooks/verificar-firma/) en tu receptor, con ese secreto. 4. **Probar.** Abre **Ver entregas** y elige **Enviar evento de prueba**. Llega un evento `webhook.test`, con `{"test": true}` en `data`, y el resultado aparece en esa misma lista. Ese evento no entra a [`GET /v1/events`](/docs/operaciones/listar-eventos/). 5. **Activar.** No hay un paso aparte: el destino nace `active` y recibe eventos desde que lo creas. Créalo cuando tu receptor ya esté listo. ## Estados de un destino | Estado | Significa | Qué puedes hacer | |---|---|---| | `active` | Recibe eventos | Probar, reenviar entregas, rotar el secreto y eliminarlo | | `disabled` | Dejó de recibir eventos tras 3 días seguidos de fallos | Reactivar, rotar el secreto y eliminarlo. El panel rechaza pruebas y reenvíos hasta reactivarlo | | `paused` | Valor reservado. Hoy ningún flujo lo asigna | Trátalo como un destino que no recibe eventos | ### Reactivar un destino Una entrega correcta reinicia la cuenta de fallos. Si los fallos siguen durante 3 días, el destino pasa a `disabled` y aparece un aviso crítico en la pantalla Avisos. Mientras esté así no se le envía nada, y lo pendiente queda `skipped`. Corrige tu receptor y elige **Reactivar** en la pantalla Webhooks. El destino vuelve a `active` y su cuenta de fallos a cero. Lo que no recibió no se envía solo: recupéralo con [`GET /v1/events`](/docs/operaciones/listar-eventos/), o reenvía desde **Ver entregas** las que aparecen ahí. ## Rotar el secreto En **Ver entregas** elige **Rotar secreto**. Recibes el secreto nuevo una sola vez. Durante 24 horas llegan dos firmas `v1=`, con el secreto nuevo y con el anterior, separadas por comas. Basta que una valide, así que despliega el secreto nuevo en ese plazo. Sólo existe un secreto anterior: rotar otra vez dentro de ese plazo retira de inmediato el que estaba en gracia. ## Reintentos Un intento falla con cualquier respuesta que no sea `2xx`, con un plazo vencido, o con un error de conexión, de certificado o de DNS. Hay un intento inicial más 10 reintentos, 11 en total, con esta espera entre ellos: 30 s, 1 min, 2 min, 5 min, 15 min, 30 min, 1 h, 2 h, 6 h, 12 h. La ventana total es de 21 h 53 min 30 s desde que termina el primer intento, más lo que tarde cada uno. Después la entrega queda `exhausted`. Un evento tarda de 20 a 120 segundos en salir. Cada destino recibe hasta 5 entregas simultáneas. ## El registro de entregas **Ver entregas** muestra las últimas entregas de un destino, con sus intentos y su resultado. Desde ahí puedes reenviar una entrega ya terminada. | Resultado | Estado de la entrega | |---|---| | Entregado | Tu receptor respondió `2xx` | | Falló | Sigue reintentando, o quedó `exhausted` | | Descartado | Quedó `skipped`, sin entrega | | Pendiente | Aún no se intenta, o está en curso | La pantalla Registros de API muestra las peticiones que tu sistema hace a la API, no estas entregas. ## El evento siempre queda guardado La entrega HTTP tiene dos excepciones. Queda `skipped` si el destino no está activo o si la cola está llena: 2,000 entregas pendientes por destino y 10,000 por empresa. Queda `exhausted` al agotar los reintentos. Por eso consulta [`GET /v1/events`](/docs/operaciones/listar-eventos/) periódicamente: 1. Pide la lista sin `cursor`: empieza en el evento más nuevo. 2. Sigue con `next_cursor` hacia el pasado hasta llegar a un evento que ya procesaste, o hasta que `next_cursor` sea `null`. 3. Procesa lo nuevo y guarda los `id` vistos o la hora del último. 4. En la siguiente consulta, repasa desde 5 minutos antes del último evento visto: dos eventos casi simultáneos pueden confirmarse en otro orden. No guardes `next_cursor` entre consultas. Sólo camina hacia el pasado y termina en `null`, así que nunca te mostraría un evento nuevo. El cursor que se guarda es el de [`GET /v1/movements`](/docs/paginacion/#movimientos-un-cursor-que-no-termina). **Variables de estas muestras: simulador público, sin cuenta** Define esto en tu terminal. Con estos valores las muestras corren tal cual contra el simulador público: ```bash export FITAL_API="https://widget.fital.xyz/explorar/v1" export FITAL_KEY="sk_test_sandbox_publica" ``` En la cuenta de prueba o en producción cambia la base, la llave y el secreto, y sustituye los `id` de ejemplo por los tuyos. Ver [Ambientes](/docs/ambientes/). ```bash # Empieza sin cursor, en el evento más nuevo. CURSOR sólo sirve para seguir esta vuelta hacia atrás: no lo guardes entre consultas. CURSOR="" curl -sS -G "$FITAL_API/events" \ -H "Authorization: Bearer $FITAL_KEY" \ --data-urlencode "limit=25" \ ${CURSOR:+--data-urlencode "cursor=$CURSOR"} ``` ```js // Empieza sin cursor, en el evento más nuevo. cursor sólo sirve para seguir esta vuelta hacia atrás: no lo guardes entre consultas. const cursor = null; const q = new URLSearchParams({ limit: '25', ...(cursor ? { cursor } : {}) }); const r = await fetch(`${process.env.FITAL_API}/events?${q}`, { headers: { Authorization: `Bearer ${process.env.FITAL_KEY}` }, }); const pagina = await r.json(); console.log(r.status, pagina.data.length, pagina.has_more, pagina.next_cursor); ``` ```python import json, os, urllib.error, urllib.parse, urllib.request # Empieza sin cursor, en el evento más nuevo. cursor sólo sirve para seguir esta vuelta hacia atrás: no lo guardes entre consultas. cursor = None q = {"limit": "25", **({"cursor": cursor} if cursor else {})} pet = urllib.request.Request(os.environ["FITAL_API"] + "/events?" + urllib.parse.urlencode(q), headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"]}) try: with urllib.request.urlopen(pet) as r: pagina = json.load(r) print(r.status, len(pagina["data"]), pagina["has_more"], pagina["next_cursor"]) except urllib.error.HTTPError as e: print(e.code, e.read().decode()) ``` ```php 25] + ($cursor ? ['cursor' => $cursor] : [])); $c = curl_init(getenv('FITAL_API') . '/events?' . $q); curl_setopt_array($c, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('FITAL_KEY')], ]); $pagina = json_decode(curl_exec($c), true); echo count($pagina['data']), ' ', var_export($pagina['has_more'], true), ' ', $pagina['next_cursor'] ?? 'null', "\n"; ``` ## Duplicados y desorden La entrega es al menos una vez y sin orden. Deduplica por el `id` del cuerpo, después de verificar la firma: la firma cubre el cuerpo y no los encabezados, así que `X-Fital-Event-Id` se puede cambiar sin invalidarla. Ordena por `sequence`: un pago `completed` puede pasar después a `returned`, y un retroceso real también se publica. Ver [Orden](/docs/webhooks/eventos/#orden). ## Errores comunes - Verificar sobre el JSON ya interpretado: cambia los bytes y la firma falla. Usa el cuerpo crudo. - Deduplicar por `X-Fital-Delivery-Id`: otro destino recibe el mismo evento con otro valor. Usa el `id` del cuerpo. - Deduplicar por `X-Fital-Event-Id`: no va firmado. Quien capture una entrega podría repetirla con otro valor dentro de la ventana de 5 minutos y procesarla dos veces. Usa el `id` del cuerpo. - Responder `2xx` hasta terminar de procesar: todo tu trabajo tendría que caber en 10 segundos. Guarda el evento, responde y procesa después. - Guardar `next_cursor` de [`GET /v1/events`](/docs/operaciones/listar-eventos/) para la siguiente consulta: no verías eventos nuevos. - Confiar en la IP de origen. La firma es la protección principal. Ver [IP de webhooks](/docs/seguridad/ip-webhooks/). ## Qué sigue [Verificar la firma](/docs/webhooks/verificar-firma/) y [Catálogo de eventos](/docs/webhooks/eventos/). --- # Inicio rápido de webhooks Un receptor que verifica firmas, corriendo en tu máquina y sin cuenta. Al terminar tendrás un receptor en tu máquina que acepta, rechaza y deduplica eventos firmados. No necesitas cuenta: un script firma los eventos con un secreto público de prueba. Necesitas Node 22, bash, curl y openssl. ## 1. Guarda el receptor Guárdalo como `webhook-receptor.mjs`: ```js import { createHmac, timingSafeEqual } from 'node:crypto'; import { createServer } from 'node:http'; const SECRETO = process.env.FITAL_WEBHOOK_SECRET ?? 'secreto_de_prueba_publico_no_usar_en_produccion'; const VENTANA_S = 300; const vistos = new Set(); // en producción, en tu base de datos function verificar(crudo, ts, firmas) { if (!/^[0-9]+$/.test(ts)) return false; // sólo dígitos: otra cosa no pasa por la ventana if (Math.abs(Date.now() / 1000 - Number(ts)) > VENTANA_S) return false; const esperada = createHmac('sha256', SECRETO).update(`${ts}.`).update(crudo).digest(); return String(firmas).split(',').some((f) => { const m = /^v1=([0-9a-f]{64})$/.exec(f.trim()); // otras versiones se ignoran return m !== null && timingSafeEqual(Buffer.from(m[1], 'hex'), esperada); }); } createServer((req, res) => { const partes = []; req.on('data', (p) => partes.push(p)); req.on('end', () => { const crudo = Buffer.concat(partes); if (!verificar(crudo, req.headers['x-fital-timestamp'], req.headers['x-fital-signature'])) { console.log('rechazado'); res.writeHead(400).end(); return; } const evento = JSON.parse(crudo); if (vistos.has(evento.id)) { // el id del cuerpo va firmado; X-Fital-Event-Id, no console.log('duplicado', evento.id); res.writeHead(200).end(); return; } vistos.add(evento.id); console.log('aceptado', evento.id, evento.type, evento.sequence); res.writeHead(200).end(); // responde ya; procesa en segundo plano }); }).listen(3000); ``` ## 2. Arráncalo ```bash node webhook-receptor.mjs ``` ## 3. Envíale eventos En otra terminal, descarga [`fital-webhook-test.sh`](/docs/herramientas/fital-webhook-test.sh) y córrelo contra tu receptor. Firma con `openssl` y no llama a FITAL ni a ningún servicio alojado. ```bash curl -sSO https://merchants.fital.xyz/docs/herramientas/fital-webhook-test.sh bash fital-webhook-test.sh http://localhost:3000/webhook payment.completed ``` ## 4. Mira el resultado El script debe imprimir esto y terminar con código `0`. Con cualquier `FALLA` termina con un código distinto de `0`: ```text ok evt_prueba_1 payment.completed -> 200 ok evt_prueba_2 payment.failed -> 200 ok evt_prueba_3 payment.returned -> 200 ok evt_prueba_mala payment.completed -> 400 ok evt_prueba_vieja payment.completed -> 400 ok evt_prueba_futura payment.completed -> 400 ok evt_prueba_alterada payment.completed -> 400 ok evt_prueba_texto payment.completed -> 400 ok evt_prueba_1 payment.completed -> 200 ok evt_prueba_1 payment.completed -> 200 ok evt_prueba_6 payment.completed -> 200 ok evt_prueba_5 payment.processing -> 200 ``` Tu receptor imprime `aceptado` cinco veces, `rechazado` cinco y `duplicado` dos. Los rechazos son una firma de otro secreto, un timestamp vencido, uno en el futuro, un cuerpo alterado después de firmar y un timestamp no numérico: el script espera un `4xx` en cada uno. Los duplicados repiten `evt_prueba_1`, el segundo con otro `X-Fital-Event-Id`: responden `200` sin procesarlo otra vez. El script sólo ve el código HTTP. Que un duplicado no se procese dos veces y que gane el `sequence` mayor lo compruebas en el registro de tu receptor. ## 5. Pásalo a un destino real 1. Publica tu receptor en HTTPS, con los [requisitos del receptor](/docs/webhooks/#requisitos-del-receptor). 2. Crea el destino en el panel y usa su secreto: pon `FITAL_WEBHOOK_SECRET` en tu entorno. Ver [Dar de alta un destino](/docs/webhooks/#dar-de-alta-un-destino). 3. Prueba con **Enviar evento de prueba**, o con [`POST /sandbox/v1/webhooks/trigger`](/docs/operaciones/emitir-evento-de-prueba/) en la cuenta de prueba. En producción, guarda los identificadores de evento en tu base y procesa en segundo plano: este receptor los guarda en memoria y sólo imprime. ## Errores comunes - `FALLA ... -> 000`: tu receptor no está corriendo o el puerto no es `3000`. - `FALLA ... se esperaba ok`: firmas con otro secreto. Sin `FITAL_WEBHOOK_SECRET`, el receptor y el script usan el mismo secreto público. - `FALLA ... se esperaba rechazo`: tu receptor aceptó una entrega que debía rechazar, o respondió `5xx`. Revisa [Verificar la firma](/docs/webhooks/verificar-firma/). - Dejar el secreto público en producción. Sólo sirve para esta prueba. ## Qué sigue [Verificar la firma](/docs/webhooks/verificar-firma/) y [Webhooks](/docs/webhooks/). --- # Verificar la firma Comprueba que cada entrega viene de FITAL, con muestras que corren tal cual. Antes de confiar en una entrega, comprueba su firma con el secreto de ese destino. Si no coincide o está vencida, responde `400` y no proceses nada. ## Qué se firma La firma es HMAC-SHA256 hexadecimal, con el secreto del destino como clave, sobre el timestamp, un punto y el cuerpo crudo: ```text hmac_sha256(secreto, ".") ``` El secreto es el que viste al crear el destino o al rotarlo. Úsalo tal como lo recibiste, como texto. ## Paso a paso 1. Lee el cuerpo como bytes, antes de que tu framework lo interprete como JSON. 2. Lee `X-Fital-Timestamp`. Rechaza la entrega si trae algo que no sean dígitos, o si difiere de tu reloj por más de 5 minutos en cualquier sentido. 3. Calcula la firma esperada. 4. Separa `X-Fital-Signature` por comas y compara cada `v1=` en tiempo constante. Basta que una coincida. 5. Ignora las versiones que no conozcas, como `v2=`: una versión nueva llegará junto a `v1=`. Cada intento lleva su propio timestamp y su propia firma. Durante una rotación llegan dos firmas `v1=`: ver [Rotar el secreto](/docs/webhooks/#rotar-el-secreto). ## Vector de prueba Estas muestras corren tal cual y responden `aceptar`. Todos sus datos son ficticios y están publicados a propósito: el secreto no pertenece a ningún destino y no sirve en ningún ambiente. Sustituye los datos por los de tu entrega para usarlas. ```bash # Vector público: todos los datos son ficticios. CUERPO='{"id":"evt_5mPqW2xZr8TnC1vH7kLdS4bJ","type":"payment.completed","sequence":3}' TS='1793800000' # X-Fital-Timestamp FIRMAS='v1=e056165e0bbfa2c675a224a926a2cb2bf184f3cfbfc53a12fc9e7f0f6f2dddfe' SECRETO_FICTICIO=a6906be5688f523ee1111208ff109c902394503a725418caf3a33c2f22a0a671 AHORA=1793800010 # en tu receptor: AHORA=$(date +%s) ESPERADA=$(printf '%s' "$TS.$CUERPO" | openssl dgst -sha256 -hmac "$SECRETO_FICTICIO" -r | cut -d' ' -f1) # Sólo dígitos: otra cosa no pasa por la ventana. if printf '%s' "$TS" | grep -Eq '^[0-9]+$' && [ "$((AHORA > TS ? AHORA - TS : TS - AHORA))" -le 300 ]; then case ",$FIRMAS," in *",v1=$ESPERADA,"*) echo aceptar ;; *) echo rechazar ;; esac else echo rechazar fi ``` ```js import { createHmac, timingSafeEqual } from 'node:crypto'; // Vector público: todos los datos son ficticios. const cuerpoCrudo = Buffer.from('{"id":"evt_5mPqW2xZr8TnC1vH7kLdS4bJ","type":"payment.completed","sequence":3}'); // los bytes tal como llegan const timestamp = '1793800000'; // X-Fital-Timestamp const firmas = 'v1=e056165e0bbfa2c675a224a926a2cb2bf184f3cfbfc53a12fc9e7f0f6f2dddfe'; // X-Fital-Signature const secretoFicticio = 'a6906be5688f523ee1111208ff109c902394503a725418caf3a33c2f22a0a671'; const ahora = 1793800010; // en tu receptor: Date.now() / 1000 function verificar(crudo, ts, cabecera, secreto, segundos) { if (!/^[0-9]+$/.test(ts)) return false; // sólo dígitos: otra cosa no pasa por la ventana if (Math.abs(segundos - Number(ts)) > 300) return false; const esperada = createHmac('sha256', secreto).update(`${ts}.`).update(crudo).digest(); return cabecera.split(',').some((f) => { const m = /^v1=([0-9a-f]{64})$/.exec(f.trim()); // otras versiones se ignoran return m !== null && timingSafeEqual(Buffer.from(m[1], 'hex'), esperada); }); } console.log(verificar(cuerpoCrudo, timestamp, firmas, secretoFicticio, ahora) ? 'aceptar' : 'rechazar'); ``` ```python import hashlib, hmac, re, time # Vector público: todos los datos son ficticios. cuerpo_crudo = b'{"id":"evt_5mPqW2xZr8TnC1vH7kLdS4bJ","type":"payment.completed","sequence":3}' # los bytes tal como llegan timestamp = '1793800000' # X-Fital-Timestamp firmas = "v1=e056165e0bbfa2c675a224a926a2cb2bf184f3cfbfc53a12fc9e7f0f6f2dddfe" secreto_ficticio = b"a6906be5688f523ee1111208ff109c902394503a725418caf3a33c2f22a0a671" ahora = 1793800010 # en tu receptor: time.time() def verificar(crudo: bytes, ts: str, cabecera: str, secreto: bytes, segundos: float) -> bool: if not re.fullmatch(r"[0-9]+", ts or ""): # sólo dígitos: otra cosa no pasa por la ventana return False if abs(segundos - int(ts)) > 300: return False esperada = hmac.new(secreto, ts.encode() + b"." + crudo, hashlib.sha256).hexdigest() for f in cabecera.split(","): m = re.fullmatch(r"v1=([0-9a-f]{64})", f.strip()) # otras versiones se ignoran if m and hmac.compare_digest(esperada, m.group(1)): return True return False print("aceptar" if verificar(cuerpo_crudo, timestamp, firmas, secreto_ficticio, ahora) else "rechazar") ``` ```php 300) return false; $esperada = hash_hmac('sha256', $ts . '.' . $crudo, $secreto); foreach (explode(',', $cabecera) as $f) { // otras versiones se ignoran if (preg_match('/^v1=([0-9a-f]{64})$/D', trim($f), $m) && hash_equals($esperada, $m[1])) return true; } return false; } echo verificar($cuerpoCrudo, $timestamp, $firmas, $secretoFicticio, $ahora) ? "aceptar\n" : "rechazar\n"; ``` En la variante con `openssl`, la comparación no es en tiempo constante: úsala para depurar, no en tu receptor. ## Comprueba los rechazos Tu receptor debe rechazar estos casos. Cambia un dato del vector y confirma que responde `rechazar`: | Caso | Qué cambiar | |---|---| | Firma de otro secreto | Una letra del secreto | | Cuerpo alterado | Un espacio al final del cuerpo | | Timestamp vencido | Adelanta el reloj más de 300 segundos | | Timestamp no numérico | Agrega una letra al timestamp | `fital-webhook-test.sh` envía esos casos a tu receptor en tu máquina, además de un timestamp en el futuro, y termina con un código distinto de `0` si alguno no se rechaza: ver el [inicio rápido](/docs/webhooks/inicio-rapido/). ## Errores comunes - Firmar el JSON interpretado o vuelto a serializar. Cambia los bytes. - Convertir el secreto a bytes desde hexadecimal. Va como texto, tal como lo recibiste. - Comparar con `==`. Usa una comparación en tiempo constante. - Convertir el timestamp a número sin validarlo. `Number("abc")` da `NaN` y se salta la ventana, y `(int)` recorta lo que sigue a los dígitos. - Mirar la ventana en un solo sentido. Un timestamp en el futuro también se rechaza. - Tomar sólo la primera firma. Durante una rotación pueden llegar dos. ## Qué sigue [Catálogo de eventos](/docs/webhooks/eventos/). --- # Catálogo de eventos Qué evento sale y cuándo. Cada evento tiene `id`, `type`, `resource_id`, `sequence`, `api_version`, `data` y `created_at`. Se consultan en [`GET /v1/events`](/docs/operaciones/listar-eventos/) y llegan por webhook, con el mismo contenido. > **Próximo.** En producción, la emisión de eventos se activa junto con la API. En la cuenta de prueba los eventos ya se emiten y se consultan en [`GET /sandbox/v1/events`](/docs/operaciones/listar-eventos/). | Evento | Cuándo sale | |---|---| | [`payment.processing`](#paymentprocessing) | El pago se aceptó y sigue en curso, o se reabrió | | [`payment.in_review`](#paymentin_review) | El pago está en revisión. No lo repitas | | [`payment.completed`](#paymentcompleted) | El pago se confirmó | | [`payment.failed`](#paymentfailed) | El pago no se hizo y no queda dinero en tránsito | | [`payment.returned`](#paymentreturned) | El pago se devolvió. Puede llegar después de `payment.completed` | | [`payment.canceled`](#paymentcanceled) | El pago se canceló | | [`deposit.received`](#depositreceived) | El dinero ya está acreditado | | [`deposit.in_review`](#depositin_review) | El depósito está en revisión | | [`deposit.returned`](#depositreturned) | El depósito se devolvió | | [`collection.paid`](#collectionpaid) | El cobro se pagó | | [`collection.expired`](#collectionexpired) | El cobro venció sin pago | | [`security.ip_allowlist.changed`](#securityip_allowlistchanged) | Cambió la lista de IP de una llave de producción | | [`system.egress_ips.changing`](#systemegress_ipschanging) | Cambiará una IP de salida de webhooks. Actualiza tu firewall antes de `effective_at` | Cada tipo está declarado en el contrato, y el cuerpo de cada uno en su sección `webhooks`. Aun así, la lista es extensible: ignora los tipos que no conozcas, sin fallar. Puedes filtrar con [`GET /v1/events?type=payment.completed`](/docs/operaciones/listar-eventos/). **Enviar evento de prueba** en el panel entrega además un evento `webhook.test`, con `{"test": true}` en `data`. No entra a [`GET /v1/events`](/docs/operaciones/listar-eventos/). ## Cuerpo de cada evento El sobre es igual en todos: sólo cambia `data`, y el webhook lleva los mismos campos que [`GET /v1/events`](/docs/operaciones/listar-eventos/), con `resource_id`. En la cuenta de prueba, `data` lleva además `livemode: false`. Un evento emitido con [`POST /sandbox/v1/webhooks/trigger`](/docs/operaciones/emitir-evento-de-prueba/) trae el `data` real de su tipo, con datos ficticios y además `simulated: true`. ### `payment.processing` El pago se aceptó y sigue en curso, o se reabrió. ```json { "id": "evt_0b16ae905ca3314bb3a098ed52717d82", "type": "payment.processing", "resource_id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "api_version": "2026-11-01", "created_at": "2026-11-04T18:01:30.000000Z", "sequence": 1, "data": { "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "status": "processing", "amount": "1500.00", "currency": "MXN", "destination_currency": "MXN", "final_amount": null, "final_currency": null, "failure_code": null, "external_ref": "pedido-7731", "updated_at": "2026-11-04T18:01:30.000Z" } } ``` ### `payment.in_review` El pago está en revisión. No lo repitas. ```json { "id": "evt_710779698b77fbc43c44c63fff818f9e", "type": "payment.in_review", "resource_id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "api_version": "2026-11-01", "created_at": "2026-11-04T18:01:30.000000Z", "sequence": 1, "data": { "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "status": "in_review", "amount": "1500.00", "currency": "MXN", "destination_currency": "MXN", "final_amount": null, "final_currency": null, "failure_code": null, "external_ref": "pedido-7731", "updated_at": "2026-11-04T18:01:30.000Z" } } ``` ### `payment.completed` El pago se confirmó. ```json { "id": "evt_9a57c9e59c247226e4d0f93e7a056f80", "type": "payment.completed", "resource_id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "api_version": "2026-11-01", "created_at": "2026-11-04T18:01:30.000000Z", "sequence": 1, "data": { "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "status": "completed", "amount": "1500.00", "currency": "MXN", "destination_currency": "MXN", "final_amount": "1500.00", "final_currency": "MXN", "failure_code": null, "external_ref": "pedido-7731", "updated_at": "2026-11-04T18:01:30.000Z" } } ``` ### `payment.failed` El pago no se hizo y no queda dinero en tránsito. `failure_code` siempre trae un código, y `payment_rejected` si no hay uno más específico. ```json { "id": "evt_bc847fdf659accc4b12b0dfe43946fdd", "type": "payment.failed", "resource_id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "api_version": "2026-11-01", "created_at": "2026-11-04T18:01:30.000000Z", "sequence": 1, "data": { "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "status": "failed", "amount": "1500.00", "currency": "MXN", "destination_currency": "MXN", "final_amount": null, "final_currency": null, "failure_code": "payment_rejected", "external_ref": "pedido-7731", "updated_at": "2026-11-04T18:01:30.000Z" } } ``` ### `payment.returned` El pago se devolvió. Puede llegar después de `payment.completed`. ```json { "id": "evt_a13d5adda1d245b713d9beaf26c5e1b5", "type": "payment.returned", "resource_id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "api_version": "2026-11-01", "created_at": "2026-11-04T18:01:30.000000Z", "sequence": 1, "data": { "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "status": "returned", "amount": "1500.00", "currency": "MXN", "destination_currency": "MXN", "final_amount": "1500.00", "final_currency": "MXN", "failure_code": null, "external_ref": "pedido-7731", "updated_at": "2026-11-04T18:01:30.000Z" } } ``` ### `payment.canceled` El pago se canceló. ```json { "id": "evt_df140300d13fad354a4dfe3bb985d7b2", "type": "payment.canceled", "resource_id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "api_version": "2026-11-01", "created_at": "2026-11-04T18:01:30.000000Z", "sequence": 1, "data": { "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "status": "canceled", "amount": "1500.00", "currency": "MXN", "destination_currency": "MXN", "final_amount": null, "final_currency": null, "failure_code": null, "external_ref": "pedido-7731", "updated_at": "2026-11-04T18:01:30.000Z" } } ``` ### `deposit.received` El dinero ya está acreditado. ```json { "id": "evt_73511a9d0102bfc0c51fedc7b0a7892e", "type": "deposit.received", "resource_id": "dpt_7bTcF2qXe9WmL4sD1nHrY6vK", "api_version": "2026-11-01", "created_at": "2026-11-04T18:01:30.000000Z", "sequence": 1, "data": { "id": "dpt_7bTcF2qXe9WmL4sD1nHrY6vK", "status": "received", "amount": "1250.00", "currency": "MXN", "credited_amount": "1250.00", "credited_currency": "MXN", "received_at": "2026-11-04T18:00:00.000Z", "reference": "7731", "concept": "Pedido 7731", "tracking_key": "CLAVE-DE-RASTREO-DEMO", "receipt_url": null, "payer": { "name": "CLIENTE FICTICIO SA DE CV", "address": null, "bank": "Banco de ejemplo", "clabe": "999180000000000022", "rfc": "XAXX010101000" } } } ``` ### `deposit.in_review` El depósito está en revisión. ```json { "id": "evt_da9fa1e2dad8c4828327a51743ed7188", "type": "deposit.in_review", "resource_id": "dpt_7bTcF2qXe9WmL4sD1nHrY6vK", "api_version": "2026-11-01", "created_at": "2026-11-04T18:01:30.000000Z", "sequence": 1, "data": { "id": "dpt_7bTcF2qXe9WmL4sD1nHrY6vK", "status": "in_review", "amount": "1250.00", "currency": "MXN", "credited_amount": null, "credited_currency": null, "received_at": "2026-11-04T18:00:00.000Z", "reference": "7731", "concept": "Pedido 7731", "tracking_key": "CLAVE-DE-RASTREO-DEMO", "receipt_url": null, "payer": { "name": "CLIENTE FICTICIO SA DE CV", "address": null, "bank": "Banco de ejemplo", "clabe": "999180000000000022", "rfc": "XAXX010101000" } } } ``` ### `deposit.returned` El depósito se devolvió. ```json { "id": "evt_e856a9e6dc10f6808b6e393a042f5297", "type": "deposit.returned", "resource_id": "dpt_7bTcF2qXe9WmL4sD1nHrY6vK", "api_version": "2026-11-01", "created_at": "2026-11-04T18:01:30.000000Z", "sequence": 1, "data": { "id": "dpt_7bTcF2qXe9WmL4sD1nHrY6vK", "status": "returned", "amount": "1250.00", "currency": "MXN", "credited_amount": null, "credited_currency": null, "received_at": "2026-11-04T18:00:00.000Z", "reference": "7731", "concept": "Pedido 7731", "tracking_key": "CLAVE-DE-RASTREO-DEMO", "receipt_url": null, "payer": { "name": "CLIENTE FICTICIO SA DE CV", "address": null, "bank": "Banco de ejemplo", "clabe": "999180000000000022", "rfc": "XAXX010101000" } } } ``` ### `collection.paid` El cobro se pagó. ```json { "id": "evt_d4b75d0d0e379b0d9930fc46035c1d5d", "type": "collection.paid", "resource_id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "api_version": "2026-11-01", "created_at": "2026-11-04T18:01:30.000000Z", "sequence": 1, "data": { "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "status": "paid", "amount": "1250.00", "destination_amount": null, "currency": "MXN", "method": "spei", "external_ref": "pedido-7731" } } ``` ### `collection.expired` El cobro venció sin pago. ```json { "id": "evt_9830df4e0486f72e0d83ce3d304b50f4", "type": "collection.expired", "resource_id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "api_version": "2026-11-01", "created_at": "2026-11-04T18:01:30.000000Z", "sequence": 1, "data": { "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "status": "expired", "amount": "1250.00", "destination_amount": null, "currency": "MXN", "method": "spei", "external_ref": "pedido-7731" } } ``` ### `security.ip_allowlist.changed` Cambió la lista de IP de una llave de producción. `resource_id` es el nombre del evento, y `key` el identificador interno de la llave. Se publica sólo en producción. ```json { "id": "evt_c39f4bb5885e3e522e4d22cb831c4c09", "type": "security.ip_allowlist.changed", "resource_id": "security.ip_allowlist.changed", "api_version": "2026-11-01", "created_at": "2026-11-04T18:01:30.000000Z", "sequence": 1, "data": { "key": "3f0c2d1e-7a54-4b1f-9c0e-1a2b3c4d5e6f", "added": [ "198.51.100.10/32" ], "removed": [] } } ``` ### `system.egress_ips.changing` Cambiará una IP de salida de webhooks. Actualiza tu firewall antes de `effective_at`. `change` es `added` si la IP empezará a usarse y `retiring` si dejará de usarse. `resource_id` empieza con `egr_` y `sequence` es `1`. Mientras un cambio esté pendiente, llega una vez a cada destino activo y suscrito, también al que se activa después del anuncio, hasta una hora antes de `effective_at`. Uno activado en esa última hora puede no recibirlo: el aviso sigue en [`GET /v1/events`](/docs/operaciones/listar-eventos/). Ver [IP de webhooks](/docs/seguridad/ip-webhooks/). ```json { "id": "evt_1fc117ce1c2d350a5a1e5dc452ef30ef", "type": "system.egress_ips.changing", "resource_id": "egr_5f0c2a9e7d3b41c68a2e9f10", "api_version": "2026-11-01", "created_at": "2026-11-04T18:01:30.000000Z", "sequence": 1, "data": { "ip": "198.51.100.10", "change": "added", "effective_at": "2026-11-20T18:00:00.000Z" } } ``` ## Montos El monto de un evento es el mismo que responde `GET` sobre ese recurso, con el formato de [Montos y fechas](/docs/montos-y-fechas/). ## Orden `sequence` crece por recurso. Si recibes `sequence` menor al último que guardaste, ignora ese evento para el estado. Un `sequence` mayor manda aunque el estado vaya hacia atrás. Los retrocesos reales de un pago se publican: `payment.processing` después de `payment.canceled` significa que se reabrió. Una lectura parcial no publica nada, salvo un rechazo ya cerrado. ## Errores comunes - Tratar `deposit.in_review` como dinero disponible. Sólo `deposit.received` significa acreditado. - Asumir que `completed` es definitivo: puede llegar `payment.returned`. - Descartar un evento porque el pago ya estaba en un estado terminal. Compara `sequence`, no el estado. - Esperar que toda devolución llegue por evento. Ver [Vigilancia de un pago completado](/docs/conceptos/#vigilancia-de-un-pago-completado). ## Qué sigue [Conciliar](/docs/recetas/conciliar/). --- # Límites Peticiones, cuerpo, tiempos y montos. Cuando superes un límite recibes `429 rate_limited` con `Retry-After`. Espera esos segundos y repite con la misma clave. ## Peticiones | Límite | Valor | Alcance | |---|---|---| | Lecturas | 20 por segundo, ráfaga de 40 | Por llave | | Escrituras | 5 por segundo, ráfaga de 10 | Por llave | | Simultáneas | 8 | Por empresa | | Por IP | 50 por segundo | Por IP de origen | | Salidas de dinero en vuelo | Una | Por empresa | En la cuenta de prueba son 5 por segundo por llave, con ráfaga de 10, en lecturas y escrituras. En el simulador público y en el borde de la cuenta de prueba son 10 por segundo por IP, con ráfaga de 20. Una segunda salida simultánea responde `409 payment_in_flight`. Repite con la misma clave cuando se resuelva la anterior. ## Cuerpo Hasta 64 KB en producción y en la cuenta de prueba, y 64 KB en el simulador público. Un cuerpo mayor responde `413`, con el código `invalid_request`. Corrige la petición: repetirla no sirve. ## Tiempos | Tipo de operación | Espera de tu cliente HTTP | |---|---| | Lecturas | 20 segundos | | Escrituras que no son pagos | 35 segundos | | Pagos | 60 segundos | Un pago responde a más tardar a los 45 segundos: si aún no hay resultado, llega `202 processing`. ## Montos No hay tope por pago: en producción, `max_amount` en [`GET /v1/limits`](/docs/operaciones/consultar-limites/) llega en `null`. Los pagos sí tienen un tope acumulado por empresa: | Tope | Valor | |---|---| | Día natural, hora de México | 100,000 MXN | | 7 días corridos | 300,000 MXN | Rebasar cualquiera de los dos responde `422 limit_exceeded`, con `retryable: false`, y el pago no llega al riel. La respuesta no dice cuál se rebasó ni cuánto queda. Para otro intento usa una clave nueva: la misma repite el rechazo. Cómo se cuenta: - Suma todo pago que pudo debitar fondos: en proceso, completado o reenviado. - Un pago rechazado libera su lugar enseguida. Uno cancelado o devuelto, cuando queda confirmado. - Un pago caído sigue contando hasta saber dónde quedó el dinero. - Repetir un pago con su misma clave no cuenta dos veces. - Otra moneda vale su equivalente en MXN al crear el pago, y ese valor ya no cambia. USD usa el tipo de cambio de ese momento. Las demás usan su débito estimado en USD, con comisión, a ese mismo tipo. - Si no hay tipo de cambio responde `503 service_unavailable`. Repite con la misma clave. - Cobros y depósitos no cuentan. El tope es igual para todas las empresas y no se aplica en la cuenta de prueba. También aplican las revisiones manuales de cumplimiento y los límites de cada riel, que responden el mismo `422 limit_exceeded`. ## Errores comunes - Reintentar un `429` sin esperar `Retry-After`. - Lanzar pagos en paralelo: una salida a la vez por empresa. Ver [Pago masivo](/docs/recetas/pago-masivo/). ## Qué sigue [Paginación](/docs/paginacion/). --- # Paginación Cursores, orden y consistencia. Las listas devuelven `data`, `has_more` y `next_cursor`. Pide la siguiente página pasando `cursor=`, con los mismos filtros. | Parámetro | Valor | |---|---| | `limit` | De 1 a 100. Por omisión, 25 | | `cursor` | El `next_cursor` de la respuesta anterior | El cursor es opaco y queda atado a tu empresa, a tu ambiente, a la ruta y a sus filtros. No lo construyas ni lo edites: cambiar un filtro exige empezar de nuevo. ## Listas sin cursor [`GET /v1/balances`](/docs/operaciones/listar-saldos/), [`GET /v1/wallets`](/docs/operaciones/listar-wallets/), [`GET /v1/receiving-accounts`](/docs/operaciones/listar-cuentas-para-recibir/), [`GET /v1/collection-methods`](/docs/operaciones/listar-metodos-de-cobro/) y [`GET /v1/webhook-endpoints`](/docs/operaciones/listar-destinos-de-webhook/) devuelven sólo `data`. ## Movimientos: un cursor que no termina En [`GET /v1/movements`](/docs/operaciones/listar-movimientos/), `next_cursor` puede venir con valor aunque `has_more` sea `false`. Guárdalo en cada página: la siguiente consulta con ese cursor sigue donde quedaste. Una vuelta es recorrer la lista hasta `has_more: false`. Al abrirla, el cursor fija su horizonte: la hora en que empezó. Lo conserva toda la vuelta, también a la mitad y aunque tardes horas en leerla. Al terminar, `next_cursor` no sigue desde la última fila: abre la siguiente vuelta desde ese horizonte, con un pequeño solape. Así ves lo que cambió mientras recorrías. - Sólo `has_more` dice si sigue. Una página puede llegar corta, o vacía, con `has_more: true`. - Un mismo `id` puede repetirse con un estado más nuevo. Deduplica por `id` y quédate con la copia de `updated_at` más reciente. Una `partial` no reemplaza a una `complete` que ya tienes. Ver [Movimientos y recibos](/docs/recetas/movimientos-y-recibos/). **Variables de estas muestras: simulador público, sin cuenta** Define esto en tu terminal. Con estos valores las muestras corren tal cual contra el simulador público: ```bash export FITAL_API="https://widget.fital.xyz/explorar/v1" export FITAL_KEY="sk_test_sandbox_publica" ``` En la cuenta de prueba o en producción cambia la base, la llave y el secreto, y sustituye los `id` de ejemplo por los tuyos. Ver [Ambientes](/docs/ambientes/). ```bash # CURSOR vive en tu almacenamiento durable; vacío la primera vez. CURSOR="" curl -sS -G "$FITAL_API/movements" \ -H "Authorization: Bearer $FITAL_KEY" \ --data-urlencode "limit=25" \ ${CURSOR:+--data-urlencode "cursor=$CURSOR"} ``` ```js // cursor vive en tu almacenamiento durable; null la primera vez. const cursor = null; const q = new URLSearchParams({ limit: '25', ...(cursor ? { cursor } : {}) }); const r = await fetch(`${process.env.FITAL_API}/movements?${q}`, { headers: { Authorization: `Bearer ${process.env.FITAL_KEY}` }, }); const pagina = await r.json(); console.log(r.status, pagina.data.length, pagina.has_more, pagina.next_cursor); ``` ```python import json, os, urllib.error, urllib.parse, urllib.request # cursor vive en tu almacenamiento durable; None la primera vez. cursor = None q = {"limit": "25", **({"cursor": cursor} if cursor else {})} pet = urllib.request.Request(os.environ["FITAL_API"] + "/movements?" + urllib.parse.urlencode(q), headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"]}) try: with urllib.request.urlopen(pet) as r: pagina = json.load(r) print(r.status, len(pagina["data"]), pagina["has_more"], pagina["next_cursor"]) except urllib.error.HTTPError as e: print(e.code, e.read().decode()) ``` ```php 25] + ($cursor ? ['cursor' => $cursor] : [])); $c = curl_init(getenv('FITAL_API') . '/movements?' . $q); curl_setopt_array($c, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('FITAL_KEY')], ]); $pagina = json_decode(curl_exec($c), true); echo count($pagina['data']), ' ', var_export($pagina['has_more'], true), ' ', $pagina['next_cursor'] ?? 'null', "\n"; ``` ## Errores comunes - `400 invalid_request` por `limit` fuera de rango. - Reusar un cursor con otros filtros: el cursor deja de ser válido. - Terminar una vuelta por una página corta o vacía: sólo termina con `has_more: false`. ## Qué sigue [Montos y fechas](/docs/montos-y-fechas/). --- # Montos y fechas Montos como texto decimal, monedas y zona horaria. Los montos son texto decimal, nunca números: `"1500.00"`. Las fechas son ISO 8601 en UTC. Los números de punto flotante pierden centavos; el texto no. ## Montos - Hasta dos decimales. `"1500"`, `"1500.5"` y `"1500.50"` son válidos. - Sin ceros a la izquierda, signo ni separadores de miles. - CLP, JPY y PYG no llevan decimales: enviarlos responde `400 invalid_request`. - En respuestas, un monto de salida puede ser negativo o traer más decimales. Sin ceros de relleno y con mínimo dos decimales: `"1500000.555"`, `"10.00"`. - Monto y moneda llegan juntos: sin moneda segura, ambos son `null`. ## Monedas Códigos en mayúsculas de 3 a 5 letras, como `MXN`, `USD` o `USDC`. ## Fechas UTC con milisegundos: `2026-11-04T18:00:00.000Z`. El `created_at` del sobre de un webhook trae microsegundos: interpreta las fechas como ISO 8601, sin fijar el número de decimales. Convierte a tu zona al mostrarlas. Los filtros `from` y `to` de [`GET /v1/movements`](/docs/operaciones/listar-movimientos/) usan el mismo formato; `from` se incluye y `to` no. ## Campos que pueden ser nulos Muchos campos de respuesta son `null` cuando el dato aún no existe. Un `final_amount` nulo no significa cero. ## Errores comunes - Serializar el monto como número en JSON: responde `400 invalid_request`. - Comparar montos como texto: `"10.00"` y `"10.000"` son iguales. Conviértelos a un tipo decimal. - Asumir exactamente dos decimales en una respuesta. ## Qué sigue [Versionado](/docs/versionado/). --- # Versionado Qué cambia sin aviso y qué rompe. Dentro de `v1` sólo entran cambios aditivos. Lo incompatible sale en una versión nueva, con 30 días de aviso mínimo. ## Cambios que no rompen Una ruta nueva, un campo opcional nuevo, un evento nuevo o un valor nuevo en una lista de estados. Tu código debe ignorar lo que no conoce: los enums son extensibles. ## Cambios que rompen Quitar un campo o una ruta, cambiar un tipo, volver obligatorio algo opcional. Sólo salen en una versión nueva. ## Avisos | Cambio | Aviso mínimo | |---|---| | Incompatible | 30 días | | IP de salida | 15 días | La única excepción a estos plazos es un arreglo urgente de seguridad, que se publica de inmediato con su nota. Los avisos salen en [Cambios](/docs/cambios/), en el RSS y en el panel. ## Rutas obsoletas Una ruta en retiro responde con `Deprecation` y `Sunset`, más el enlace a su nota. Vigila esos encabezados. ## Errores comunes - Fallar ante un campo desconocido. Ignóralo. - Hacer `switch` sin rama por omisión sobre un estado. ## Qué sigue [Cambios](/docs/cambios/). --- # Cobros Crear un cobro y seguirlo hasta que se pague. Un cobro genera la instrucción que tu cliente usa para pagarte. Eliges método y monto; FITAL devuelve la instrucción y el estado. ## 1. Consulta los métodos [`GET /v1/collection-methods`](/docs/operaciones/listar-metodos-de-cobro/) da país, moneda, método, rango de monto y `payer_fields`, lo que el método pide de quien paga. Usa el valor de `method` tal cual. ## 2. Estima, si quieres [`POST /v1/collections/estimate`](/docs/operaciones/estimar-cobro/) devuelve lo que recibirías y la comisión. Lleva firma, sin clave de idempotencia. ## 3. Crea el cobro Con [`POST /v1/collections`](/docs/operaciones/crear-cobro/), envía `amount`, lo que paga tu cliente, o `destination_amount`, lo que quieres recibir, y en `payer` lo que pida el método. **Variables de estas muestras: simulador público, sin cuenta** Define esto en tu terminal. Con estos valores las muestras corren tal cual contra el simulador público: ```bash export FITAL_API="https://widget.fital.xyz/explorar/v1" export FITAL_KEY="sk_test_sandbox_publica" export FITAL_SIGNING_SECRET="secreto_de_firma_de_prueba_publico" ``` En la cuenta de prueba o en producción cambia la base, la llave y el secreto, y sustituye los `id` de ejemplo por los tuyos. Ver [Ambientes](/docs/ambientes/). ```bash CUERPO='{"currency":"MXN","method":"spei","amount":"1250.00","payer":{"firstName":"CLIENTE","lastName":"FICTICIO","email":"pagos@empresa-ejemplo.com"},"external_ref":"pedido-7731"}' CLAVE="cobro-pedido-7731" TS=$(date +%s) HASH=$(printf '%s' "$CUERPO" | openssl dgst -sha256 -r | cut -d' ' -f1) FIRMA=$(printf '%s\n%s\n%s\n%s\n%s' "$TS" POST /v1/collections "$HASH" "$CLAVE" \ | openssl dgst -sha256 -hmac "$FITAL_SIGNING_SECRET" -r | cut -d' ' -f1) curl -sS -X POST "$FITAL_API/collections" \ -H "Authorization: Bearer $FITAL_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $CLAVE" \ -H "X-Fital-Timestamp: $TS" \ -H "X-Fital-Signature: v1=$FIRMA" \ -d "$CUERPO" ``` ```js import { createHash, createHmac } from 'node:crypto'; const cuerpo = JSON.stringify({"currency":"MXN","method":"spei","amount":"1250.00","payer":{"firstName":"CLIENTE","lastName":"FICTICIO","email":"pagos@empresa-ejemplo.com"},"external_ref":"pedido-7731"}); const clave = 'cobro-pedido-7731'; const ts = String(Math.floor(Date.now() / 1000)); const hash = createHash('sha256').update(cuerpo).digest('hex'); const firma = createHmac('sha256', process.env.FITAL_SIGNING_SECRET) .update([ts, 'POST', '/v1/collections', hash, clave].join('\n')).digest('hex'); const r = await fetch(`${process.env.FITAL_API}/collections`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.FITAL_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': clave, 'X-Fital-Timestamp': ts, 'X-Fital-Signature': `v1=${firma}`, }, body: cuerpo, }); console.log(r.status, await r.json()); ``` ```python import hashlib, hmac, json, os, time, urllib.error, urllib.request datos = {"currency":"MXN","method":"spei","amount":"1250.00","payer":{"firstName":"CLIENTE","lastName":"FICTICIO","email":"pagos@empresa-ejemplo.com"},"external_ref":"pedido-7731"} cuerpo = json.dumps(datos).encode() clave = "cobro-pedido-7731" ts = str(int(time.time())) canon = "\n".join([ts, "POST", "/v1/collections", hashlib.sha256(cuerpo).hexdigest(), clave]) firma = hmac.new(os.environ["FITAL_SIGNING_SECRET"].encode(), canon.encode(), hashlib.sha256).hexdigest() pet = urllib.request.Request(os.environ["FITAL_API"] + "/collections", method="POST", data=cuerpo, headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"], "Content-Type": "application/json", "Idempotency-Key": clave, "X-Fital-Timestamp": ts, "X-Fital-Signature": "v1=" + firma}) try: with urllib.request.urlopen(pet) as r: print(r.status, json.load(r)) except urllib.error.HTTPError as e: print(e.code, e.read().decode()) ``` ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('FITAL_KEY'), 'Content-Type: application/json', 'Idempotency-Key: ' . $clave, 'X-Fital-Timestamp: ' . $ts, 'X-Fital-Signature: v1=' . $firma, ], CURLOPT_POSTFIELDS => $cuerpo, ]); echo curl_exec($c), "\n"; ``` La respuesta trae `instruction`, con `type` y `value`, y `expires_at`. [`GET /v1/collections/{id}`](/docs/operaciones/consultar-cobro/) devuelve lo mismo. Con `202`, el alta aún no se confirma y ambos llegan en `null`: repite con la misma clave, y en cuanto exista el resultado lo recibes. Los datos de quien paga van en `payer`, y `payer_fields` manda: cada fila da la clave en `name`, si es obligatoria en `required`, los valores admitidos en `options` y el prefijo telefónico en `phone_prefix`. El país se deduce de la moneda: envía `countrySymbol`, en 2 letras, sólo si quien paga es de otro país. Sin un campo `required`, o con un valor fuera de `options`, la respuesta es `400 invalid_request`, sin crear nada: ```json { "error": { "code": "invalid_request", "message": "Faltan datos de quien paga", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "payer.email": "Obligatorio en este método, o con un valor no admitido" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` Ese rechazo no cierra la clave, pero la clave conserva su cuerpo: repetir el mismo cuerpo da el mismo `400`, y el cuerpo corregido necesita **otra** `Idempotency-Key`. Con la misma responde `422 idempotency_key_reused`. Si la comprobación no se puede hacer a tiempo en producción, el cobro sigue y decide el riel. ## 4. Sigue el estado | Estado | Significado | |---|---| | `processing` | El alta sigue en curso | | `pending` | Espera el pago | | `paid` | Pagado | | `expired` | Venció sin pago | | `failed` | No se pudo crear o completar | Consulta [`GET /v1/collections/{id}`](/docs/operaciones/consultar-cobro/). Los eventos `collection.paid` y `collection.expired` llegan por webhook. ## Errores comunes - `403 not_enabled`: el método no está habilitado para tu cuenta. - `422 amount_out_of_range`: revisa `min_amount` y `max_amount`. - `400 invalid_request`: falta `amount` o `destination_amount`, o `details.fields` nombra un campo `payer.`. - Corregir `payer` y repetir con la misma clave: responde `422 idempotency_key_reused`. ## Qué sigue [Quién pagó](/docs/recetas/quien-pago/). ## Cobros en efectivo `efectivo` cobra en tiendas de México, en MXN, de 20.00 a 10,000.00 por operación. Está disponible bajo solicitud: pídelo a FITAL. Sale en [`GET /v1/collection-methods`](/docs/operaciones/listar-metodos-de-cobro/) cuando tu empresa lo tiene activado; sin activación, crear el cobro responde `403 not_enabled`. - Se pide con `amount`, no con `destination_amount`. `payer` no lleva nada: quien paga no da datos. - `instruction.type` llega en `REFERENCE`, y `instruction.value` es la referencia que quien paga da en la tienda. Se entrega sólo la referencia. - Sin `expires_at`, el cobro vence a los 3 días. - `paid` y `collection.paid` dicen que la tienda confirmó el pago. El abono a tu cuenta llega después, y [`POST /v1/collections/estimate`](/docs/operaciones/estimar-cobro/) no promete cuánto recibirás: `receive_amount` llega en `null`. --- # Conciliar Cuadrar tus movimientos contra tus registros. Concilia con [`GET /v1/movements`](/docs/operaciones/listar-movimientos/) como fuente de lo que pasó en tu cuenta, y con [`GET /v1/events`](/docs/operaciones/listar-eventos/) para saber qué cambió desde tu última lectura. Cada consulta de eventos empieza sin cursor, en el más nuevo: ver [El evento siempre queda guardado](/docs/webhooks/#el-evento-siempre-queda-guardado). ## Método 1. Consulta movimientos con un cursor durable. Ver [Movimientos y recibos](/docs/recetas/movimientos-y-recibos/). 2. Empata cada movimiento con tu registro. `payment_id` es el `pay_` del pago de origen y `external_ref` es tu referencia. `payment_id` viene en `null` en depósitos, en lo que no salió de esta API y en un cobro, que sí trae `external_ref`. 3. Revisa `read_status`: `partial` significa que el dato aún no está completo y debes releerlo. 4. Trata `returned` después de `completed` como una reversa, y cualquier otro retroceso publicado como una corrección. 5. Guarda el recibo de los movimientos terminales. ## Eventos Pide la lista sin `cursor` y recorre con `next_cursor` hacia el pasado hasta llegar a un evento que ya procesaste. No guardes `next_cursor` entre consultas: sólo camina hacia el pasado. **Variables de estas muestras: simulador público, sin cuenta** Define esto en tu terminal. Con estos valores las muestras corren tal cual contra el simulador público: ```bash export FITAL_API="https://widget.fital.xyz/explorar/v1" export FITAL_KEY="sk_test_sandbox_publica" ``` En la cuenta de prueba o en producción cambia la base, la llave y el secreto, y sustituye los `id` de ejemplo por los tuyos. Ver [Ambientes](/docs/ambientes/). ```bash # Empieza sin cursor, en el evento más nuevo. CURSOR sólo sirve para seguir esta vuelta hacia atrás: no lo guardes entre consultas. CURSOR="" curl -sS -G "$FITAL_API/events" \ -H "Authorization: Bearer $FITAL_KEY" \ --data-urlencode "limit=25" \ ${CURSOR:+--data-urlencode "cursor=$CURSOR"} ``` ```js // Empieza sin cursor, en el evento más nuevo. cursor sólo sirve para seguir esta vuelta hacia atrás: no lo guardes entre consultas. const cursor = null; const q = new URLSearchParams({ limit: '25', ...(cursor ? { cursor } : {}) }); const r = await fetch(`${process.env.FITAL_API}/events?${q}`, { headers: { Authorization: `Bearer ${process.env.FITAL_KEY}` }, }); const pagina = await r.json(); console.log(r.status, pagina.data.length, pagina.has_more, pagina.next_cursor); ``` ```python import json, os, urllib.error, urllib.parse, urllib.request # Empieza sin cursor, en el evento más nuevo. cursor sólo sirve para seguir esta vuelta hacia atrás: no lo guardes entre consultas. cursor = None q = {"limit": "25", **({"cursor": cursor} if cursor else {})} pet = urllib.request.Request(os.environ["FITAL_API"] + "/events?" + urllib.parse.urlencode(q), headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"]}) try: with urllib.request.urlopen(pet) as r: pagina = json.load(r) print(r.status, len(pagina["data"]), pagina["has_more"], pagina["next_cursor"]) except urllib.error.HTTPError as e: print(e.code, e.read().decode()) ``` ```php 25] + ($cursor ? ['cursor' => $cursor] : [])); $c = curl_init(getenv('FITAL_API') . '/events?' . $q); curl_setopt_array($c, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('FITAL_KEY')], ]); $pagina = json_decode(curl_exec($c), true); echo count($pagina['data']), ' ', var_export($pagina['has_more'], true), ' ', $pagina['next_cursor'] ?? 'null', "\n"; ``` > **Próximo.** En producción, [`GET /v1/events`](/docs/operaciones/listar-eventos/) recibe eventos desde que se activa la API. En la cuenta de prueba ya los recibe, en [`GET /sandbox/v1/events`](/docs/operaciones/listar-eventos/). ## Devoluciones tardías Una devolución puede no llegar sola: fuera de `latam` depende del aviso del riel, en cualquier plazo, y en `latam` la vigilancia dura 120 días. Si tu cierre depende de un pago, consulta [`GET /v1/payments/{id}`](/docs/operaciones/consultar-pago/), que lee en vivo. Ver [Vigilancia de un pago completado](/docs/conceptos/#vigilancia-de-un-pago-completado). ## Errores comunes - Cerrar el día con movimientos en `processing`. - Usar para eventos un cursor durable, como el de movimientos. En [`GET /v1/events`](/docs/operaciones/listar-eventos/) no verías lo nuevo. - Ignorar `returned`: un pago `completed` puede revertirse después. - Conciliar con [`GET /v1/payments`](/docs/operaciones/listar-pagos/): es una vista y puede ir atrasada. Usa movimientos y el detalle del pago. ## Qué sigue [Reintentos](/docs/recetas/reintentos/). --- # Cuentas para recibir Abrir una cuenta en otra moneda y dar sus datos a quien te paga. Una cuenta para recibir son los datos que le das a quien te paga. Tu CLABE en MXN ya viene con tu empresa; las demás se abren por API, una por moneda. ## 1. Consulta las que ya tienes [`GET /v1/receiving-accounts`](/docs/operaciones/listar-cuentas-para-recibir/) da cada cuenta, con su moneda. Pide el scope `accounts:read`. ## 2. Abre la que falta [`POST /v1/receiving-accounts`](/docs/operaciones/abrir-cuenta-para-recibir/) con `currency` en `USD`, `EUR`, `BRL`, `GBP` o `COP`. Pide el scope `accounts:write`, firma e `Idempotency-Key`. Las llaves que ya tienes no traen ese scope: crea una que lo incluya. | Respuesta | Qué significa | Qué haces | |---|---|---| | `201` | Se abrió, con `status: active` | Usa sus datos | | `200` | Ya existía, con `already_existed: true` | Usa sus datos: es la misma cuenta | | `202` | Aún no se confirma, con `status: processing` y los datos en `null` | No compartas nada. Repite con la misma clave | | `403 not_enabled` | Esa moneda no está activada para tu empresa | Pídela a tu ejecutivo FITAL | | `409 receiving_account_in_progress` | Otra alta de esa moneda sigue en curso | Reintenta en unos segundos | | `422 currency_not_supported` | Esa moneda no se abre por API, como `MXN` | Usa tu CLABE | | `422 receiving_account_rejected` | No se pudo abrir | Contacta a tu ejecutivo FITAL | ## 3. Comparte los datos de cada moneda | Moneda | Campos | |---|---| | `USD` | `account_number` y `routing_number` | | `EUR` | `iban` y `bic` | | `GBP` | `account_number` y `sort_code` | | `BRL` | `pix_key` y `br_code` | | `COP` | `account_number` y `bre_b_key` | Un campo que no aplica llega en `null`. `holder` también puede llegar en `null`: no lo sustituyas con otro nombre. --- # Direcciones de depósito Crea una dirección para recibir un token por una red. Una dirección de depósito recibe un token por una red. Se crea con [`POST /v1/deposit-addresses`](/docs/operaciones/crear-direccion-de-deposito/), con `chain` y `currency`, y la respuesta trae también `warning`. Las combinaciones disponibles están en [Cobertura](/docs/cobertura/#redes-y-tokens). **Variables de estas muestras: simulador público, sin cuenta** Define esto en tu terminal. Con estos valores las muestras corren tal cual contra el simulador público: ```bash export FITAL_API="https://widget.fital.xyz/explorar/v1" export FITAL_KEY="sk_test_sandbox_publica" export FITAL_SIGNING_SECRET="secreto_de_firma_de_prueba_publico" ``` En la cuenta de prueba o en producción cambia la base, la llave y el secreto, y sustituye los `id` de ejemplo por los tuyos. Ver [Ambientes](/docs/ambientes/). ```bash CUERPO='{"chain":"polygon","currency":"USDC"}' CLAVE="direccion-tesoreria-01" TS=$(date +%s) HASH=$(printf '%s' "$CUERPO" | openssl dgst -sha256 -r | cut -d' ' -f1) FIRMA=$(printf '%s\n%s\n%s\n%s\n%s' "$TS" POST /v1/deposit-addresses "$HASH" "$CLAVE" \ | openssl dgst -sha256 -hmac "$FITAL_SIGNING_SECRET" -r | cut -d' ' -f1) curl -sS -X POST "$FITAL_API/deposit-addresses" \ -H "Authorization: Bearer $FITAL_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $CLAVE" \ -H "X-Fital-Timestamp: $TS" \ -H "X-Fital-Signature: v1=$FIRMA" \ -d "$CUERPO" ``` ```js import { createHash, createHmac } from 'node:crypto'; const cuerpo = JSON.stringify({"chain":"polygon","currency":"USDC"}); const clave = 'direccion-tesoreria-01'; const ts = String(Math.floor(Date.now() / 1000)); const hash = createHash('sha256').update(cuerpo).digest('hex'); const firma = createHmac('sha256', process.env.FITAL_SIGNING_SECRET) .update([ts, 'POST', '/v1/deposit-addresses', hash, clave].join('\n')).digest('hex'); const r = await fetch(`${process.env.FITAL_API}/deposit-addresses`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.FITAL_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': clave, 'X-Fital-Timestamp': ts, 'X-Fital-Signature': `v1=${firma}`, }, body: cuerpo, }); console.log(r.status, await r.json()); ``` ```python import hashlib, hmac, json, os, time, urllib.error, urllib.request datos = {"chain":"polygon","currency":"USDC"} cuerpo = json.dumps(datos).encode() clave = "direccion-tesoreria-01" ts = str(int(time.time())) canon = "\n".join([ts, "POST", "/v1/deposit-addresses", hashlib.sha256(cuerpo).hexdigest(), clave]) firma = hmac.new(os.environ["FITAL_SIGNING_SECRET"].encode(), canon.encode(), hashlib.sha256).hexdigest() pet = urllib.request.Request(os.environ["FITAL_API"] + "/deposit-addresses", method="POST", data=cuerpo, headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"], "Content-Type": "application/json", "Idempotency-Key": clave, "X-Fital-Timestamp": ts, "X-Fital-Signature": "v1=" + firma}) try: with urllib.request.urlopen(pet) as r: print(r.status, json.load(r)) except urllib.error.HTTPError as e: print(e.code, e.read().decode()) ``` ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('FITAL_KEY'), 'Content-Type: application/json', 'Idempotency-Key: ' . $clave, 'X-Fital-Timestamp: ' . $ts, 'X-Fital-Signature: v1=' . $firma, ], CURLOPT_POSTFIELDS => $cuerpo, ]); echo curl_exec($c), "\n"; ``` La respuesta trae `chain` en minúsculas y `currency` en mayúsculas, sin importar cómo los pediste. ## Antes de compartirla - En `solana`, `tron`, `tempo`, `linea` y `xdc` un depósito rechazado no regresa automáticamente. Lo dice también el `warning` de esas direcciones. Tenlo en cuenta antes de usarlas para montos grandes. - `stellar` no está disponible. - Otro token enviado a una dirección puede no acreditarse. Respeta `chain`, `currency` y `warning` de la respuesta. ## En qué se acredita - EURC se queda en EURC. En [`GET /v1/balances`](/docs/operaciones/listar-saldos/) suma a la fila `EUR`, y el depósito trae `credited_currency: EURC`. - USDC, USDT y PYUSD se acreditan en dólares: suman a la fila `USD`. Lee `credited_amount` y `credited_currency` de cada depósito. ## Estados `processing` mientras se crea y `active` cuando ya recibe. Consulta con [`GET /v1/deposit-addresses/{id}`](/docs/operaciones/consultar-direccion-de-deposito/). ## Errores comunes - `422 pair_not_supported`: esa red no recibe ese token. - `422 deposit_address_rejected`: la dirección se rechazó. Revisa `chain` y `currency`. - Compartir la dirección con `processing`: `address` todavía es `null`. ## Qué sigue [Quién pagó](/docs/recetas/quien-pago/). --- # Movimientos y recibos Lista cada entrada y salida de tu cuenta y descarga su recibo. [`GET /v1/movements`](/docs/operaciones/listar-movimientos/) es el historial de tu cuenta. [`GET /v1/movements/{id}/receipt`](/docs/operaciones/descargar-recibo/) da el recibo de un movimiento terminal, en PDF o en JSON. ## Listar Filtros: `from`, `to`, `type` y `currency`. Los tipos son `payment`, `deposit`, `crypto`, `internal`, `card` y `other`: cada uno se explica en [Valores de `type`](/docs/operaciones/listar-movimientos/#valores-de-type). **Variables de estas muestras: simulador público, sin cuenta** Define esto en tu terminal. Con estos valores las muestras corren tal cual contra el simulador público: ```bash export FITAL_API="https://widget.fital.xyz/explorar/v1" export FITAL_KEY="sk_test_sandbox_publica" ``` En la cuenta de prueba o en producción cambia la base, la llave y el secreto, y sustituye los `id` de ejemplo por los tuyos. Ver [Ambientes](/docs/ambientes/). ```bash # CURSOR vive en tu almacenamiento durable; vacío la primera vez. CURSOR="" curl -sS -G "$FITAL_API/movements" \ -H "Authorization: Bearer $FITAL_KEY" \ --data-urlencode "limit=25" \ ${CURSOR:+--data-urlencode "cursor=$CURSOR"} ``` ```js // cursor vive en tu almacenamiento durable; null la primera vez. const cursor = null; const q = new URLSearchParams({ limit: '25', ...(cursor ? { cursor } : {}) }); const r = await fetch(`${process.env.FITAL_API}/movements?${q}`, { headers: { Authorization: `Bearer ${process.env.FITAL_KEY}` }, }); const pagina = await r.json(); console.log(r.status, pagina.data.length, pagina.has_more, pagina.next_cursor); ``` ```python import json, os, urllib.error, urllib.parse, urllib.request # cursor vive en tu almacenamiento durable; None la primera vez. cursor = None q = {"limit": "25", **({"cursor": cursor} if cursor else {})} pet = urllib.request.Request(os.environ["FITAL_API"] + "/movements?" + urllib.parse.urlencode(q), headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"]}) try: with urllib.request.urlopen(pet) as r: pagina = json.load(r) print(r.status, len(pagina["data"]), pagina["has_more"], pagina["next_cursor"]) except urllib.error.HTTPError as e: print(e.code, e.read().decode()) ``` ```php 25] + ($cursor ? ['cursor' => $cursor] : [])); $c = curl_init(getenv('FITAL_API') . '/movements?' . $q); curl_setopt_array($c, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('FITAL_KEY')], ]); $pagina = json_decode(curl_exec($c), true); echo count($pagina['data']), ' ', var_export($pagina['has_more'], true), ' ', $pagina['next_cursor'] ?? 'null', "\n"; ``` Guarda `next_cursor` en cada página, aunque `has_more` sea `false`: es el punto desde el que seguirás mañana. Un mismo `id` puede repetirse con un estado más nuevo: deduplica por `id` y quédate con la copia de `updated_at` más reciente. Ver [Paginación](/docs/paginacion/#movimientos-un-cursor-que-no-termina). ## Lectura parcial `read_status: "partial"` significa que el dato aún no está completo. Relee ese movimiento más tarde, y no dejes que reemplace a una fila `complete` que ya tengas. Con `partial` no concluyas nada de `status`: suele salir `processing` aunque el pago ya hubiera terminado, y eso no es un retroceso. ## Recibo Con `Accept: application/json` obtienes los datos. Sin ese encabezado obtienes un PDF. La respuesta no se debe guardar en cachés compartidos. ```bash curl -sS "$FITAL_API/movements/mov_2hKsV9tBq4XzM7nD1cYpL6rF/receipt" \ -H "Authorization: Bearer $FITAL_KEY" \ -H "Accept: application/json" ``` ```js const r = await fetch(`${process.env.FITAL_API}/movements/mov_2hKsV9tBq4XzM7nD1cYpL6rF/receipt`, { headers: { Authorization: `Bearer ${process.env.FITAL_KEY}`, 'Accept': 'application/json' }, }); console.log(r.status, await r.json()); ``` ```python import json, os, urllib.error, urllib.request pet = urllib.request.Request(os.environ["FITAL_API"] + "/movements/mov_2hKsV9tBq4XzM7nD1cYpL6rF/receipt", headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"], "Accept": "application/json"}) try: with urllib.request.urlopen(pet) as r: print(r.status, json.load(r)) except urllib.error.HTTPError as e: print(e.code, e.read().decode()) ``` ```php true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('FITAL_KEY'), 'Accept: application/json'], ]); echo curl_exec($c), "\n"; ``` Si el movimiento aún no es terminal, la respuesta es `409 receipt_not_available`. `completed_at` llega en `null` si el movimiento no terminó en `completed`: un recibo de `failed`, `returned` o `canceled` no trae fecha de completado. ## Errores comunes - Pedir el recibo de un movimiento en `processing`. - Reiniciar el cursor cada día: vuelves a leer todo. - Guardar el cursor sólo al terminar la vuelta: si tu proceso se cae, la repites entera. - `403 forbidden_scope`: necesitas `movements:read`. ## Qué sigue [Conciliar](/docs/recetas/conciliar/). --- # Pago ACH Pagar en dólares a una cuenta de Estados Unidos por ACH. Para pagar por ACH registras la cuenta con su domicilio, la apruebas en el panel y creas el pago en `USD`. ## Requisitos | Requisito | Valor | |---|---| | `rail` | `usd_ach` | | País | Estados Unidos | | Moneda del pago | `USD` | | Datos de la cuenta | Número de ruta, cuenta y domicilio | | Disponible si | `usd_ach` aparece en `rails` de [`GET /v1/limits`](/docs/operaciones/consultar-limites/). Si no, `403 rail_not_enabled` | | Cuenta de prueba | No se simula: responde `403 rail_not_enabled` | ## Registra el beneficiario Con [`POST /v1/beneficiaries`](/docs/operaciones/crear-beneficiario/): | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `account_number` | texto | Sí | Número de cuenta, de 4 a 17 dígitos. | | `account_type` | texto | Sí | Tipo de cuenta. `checking`: Cuenta de cheques o corriente. `savings`: Cuenta de ahorro. | | `address` | objeto | Sí | Domicilio del titular. | | `address.city` | texto | Sí | Ciudad. Hasta 100 caracteres. | | `address.country` | texto | Sí | País, en código de 3 letras. 3 caracteres. | | `address.postal_code` | texto | Sí | Código postal. Hasta 20 caracteres. | | `address.state` | texto | Sí | Estado o provincia. Hasta 100 caracteres. | | `address.street_line_1` | texto | Sí | Calle y número. De 4 a 35 caracteres. | | `holder_name` | texto | Sí | Titular de la cuenta destino. Hasta 140 caracteres. | | `rail` | texto | Sí | Riel de pago. Decide qué campos se piden. `usd_ach`: ACH, en Estados Unidos. | | `routing_number` | texto | Sí | Número de ruta, 9 dígitos. | ```json { "rail": "usd_ach", "holder_name": "PROVEEDOR FICTICIO LLC", "routing_number": "999999999", "account_number": "000123456789", "account_type": "checking", "address": { "street_line_1": "100 Calle Ficticia", "city": "Ciudad Ficticia", "state": "TX", "postal_code": "00000", "country": "USA" } } ``` `address.street_line_1` debe incluir el número de la calle. Sin él, el alta responde `400 invalid_request` en `address`: se valida al registrar para que un pago no falle después por ese dato. ## Paga Aprueba el beneficiario en el panel y crea el pago con [`POST /v1/payments`](/docs/operaciones/crear-pago/), con `currency` en `USD`. Si te importa el débito, cotiza antes con [`POST /v1/quotes`](/docs/operaciones/crear-cotizacion/). El resto del flujo es igual al de [Pago SPEI](/docs/recetas/pago-spei/). ## Errores comunes - `400 invalid_request`: revisa `details.fields`; el caso típico es `address` sin número de calle. - `403 rail_not_enabled`: el riel no está habilitado para tu cuenta. - `422 currency_not_supported`: el pago no va en `USD`. ## Qué sigue [Pago wire](/docs/recetas/pago-wire/). --- # Pago Bre-B Pagar en pesos colombianos con una llave Bre-B. Para pagar en Colombia registras la llave Bre-B del titular, la apruebas en el panel y creas el pago en `COP`. ## Requisitos | Requisito | Valor | |---|---| | `rail` | `cop` | | País | Colombia | | Moneda del pago | `COP` | | Datos de la cuenta | Llave Bre-B | | Disponible si | `cop` aparece en `rails` de [`GET /v1/limits`](/docs/operaciones/consultar-limites/). Si no, `403 rail_not_enabled` | | Cuenta de prueba | No se simula: responde `403 rail_not_enabled` | ## Registra el beneficiario | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `bre_b_key` | texto | Sí | Llave Bre-B del titular. Hasta 100 caracteres. | | `holder_name` | texto | Sí | Titular de la cuenta destino. Hasta 140 caracteres. | | `rail` | texto | Sí | Riel de pago. Decide qué campos se piden. `cop`: Bre-B, en Colombia. | ```json { "rail": "cop", "holder_name": "PROVEEDOR FICTICIO SAS", "bre_b_key": "llave-bre-b-de-ejemplo" } ``` ## Paga Aprueba el beneficiario en el panel y crea el pago con [`POST /v1/payments`](/docs/operaciones/crear-pago/), con `currency` en `COP`. El resto del flujo es igual al de [Pago SPEI](/docs/recetas/pago-spei/). ## Errores comunes - `422 invalid_beneficiary`: el riel rechazó la llave. - `422 currency_not_supported`: el pago no va en `COP`. - Buscar `rail: "bre_b"`: el valor es `cop`. ## Qué sigue [Pago PIX](/docs/recetas/pago-pix/). --- # Pago FPS Pagar en libras a una cuenta de Reino Unido por Faster Payments. Para pagar por Faster Payments registras el número de cuenta y el sort code, los apruebas en el panel y creas el pago en `GBP`. ## Requisitos | Requisito | Valor | |---|---| | `rail` | `gbp_fps` | | País | Reino Unido | | Moneda del pago | `GBP` | | Datos de la cuenta | Número de cuenta y sort code | | Disponible si | `gbp_fps` aparece en `rails` de [`GET /v1/limits`](/docs/operaciones/consultar-limites/). Si no, `403 rail_not_enabled` | | Cuenta de prueba | No se simula: responde `403 rail_not_enabled` | ## Registra el beneficiario | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `account_number` | texto | Sí | Número de cuenta, 8 dígitos. | | `holder_name` | texto | Sí | Titular de la cuenta destino. Hasta 140 caracteres. | | `rail` | texto | Sí | Riel de pago. Decide qué campos se piden. `gbp_fps`: Faster Payments, en Reino Unido. | | `sort_code` | texto | Sí | Sort code, 6 dígitos. | ```json { "rail": "gbp_fps", "holder_name": "PROVEEDOR FICTICIO LTD", "account_number": "00000000", "sort_code": "000000" } ``` `sort_code` va sin guiones. ## Paga Aprueba el beneficiario en el panel y crea el pago con [`POST /v1/payments`](/docs/operaciones/crear-pago/), con `currency` en `GBP`. El resto del flujo es igual al de [Pago SPEI](/docs/recetas/pago-spei/). ## Errores comunes - `400 invalid_request`: el sort code lleva guiones. Envía sólo dígitos. - `422 invalid_beneficiary`: el riel rechazó la cuenta. ## Qué sigue [Pago Bre-B](/docs/recetas/pago-bre-b/). --- # Pago LatAm Pagar en moneda local en otros países de América Latina. El riel `latam` paga por transferencia local en la moneda de cada país. Los datos de la cuenta cambian por país, y el pago se confirma cuando el banco de destino lo acredita. ## Requisitos | Requisito | Valor | |---|---| | `rail` | `latam` | | País | Ver [Requisitos por país](#requisitos-por-país) | | Moneda del pago | La del país | | Datos de la cuenta | Cuenta local, según el país | | Disponible si | `latam` aparece en `rails` de [`GET /v1/limits`](/docs/operaciones/consultar-limites/), y tu empresa tiene el riel activo. Si no, `403 rail_not_enabled` o `403 not_enabled` | | Cuenta de prueba | No se simula: responde `403 rail_not_enabled` | ## Requisitos por país | País | `country` | `currency` | `account_number` | `entity` | `account_type` | `document_type` | |---|---|---|---|---|---|---| | Argentina | `AR` | `ARS` | CBU o CVU, 22 dígitos | No | No | `DNI`, `CUIL`, `CUIT` | | Bolivia | `BO` | `BOB` | De 5 a 20 dígitos | Obligatorio | Opcional | `CED_CIU`, `NIT` | | Chile | `CL` | `CLP` | De 5 a 20 dígitos | Obligatorio | Opcional | `RUT`, `PP` | | Perú | `PE` | `PEN` | CCI, 20 dígitos | No | Obligatorio | `DNI`, `RUC`, `CED_EXT` | La tabla orienta. [`GET /v1/latam/countries`](/docs/operaciones/listar-paises-latam/) manda: sólo trae los países de tu empresa, con la configuración vigente, y `available` dice si alguno está restringido por ahora. - [`GET /v1/latam/countries`](/docs/operaciones/listar-paises-latam/) dice qué pide cada país: moneda, documentos, largo del número, `entity_required` y `account_type_required`. - `entity` es el código del banco, en los países que no lo deducen del número de cuenta. Los valores salen de [`GET /v1/latam/countries/{country}/entities`](/docs/operaciones/listar-bancos-latam/): envía su `code` tal cual. - `account_type` es `checking` o `savings`. Perú lo exige: sin él, el alta se rechaza. - `document_type` y `document` identifican al titular. Si los envías, usa un tipo de la tabla: otro valor lo rechaza el banco o el riel. - `country` lleva 2 letras y `currency` 3. La moneda debe ser la del país. ## Registra el beneficiario | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `account_number` | texto | Sí | Número de cuenta. [`GET /v1/latam/countries`](/docs/operaciones/listar-paises-latam/) dice qué pide cada país. De 4 a 40 caracteres. | | `account_type` | texto | No | Tipo de cuenta, si el país lo pide. [`GET /v1/latam/countries`](/docs/operaciones/listar-paises-latam/) dice qué pide cada país. `checking`: Cuenta de cheques o corriente. `savings`: Cuenta de ahorro. | | `country` | texto | Sí | País de la cuenta, en código de 2 letras. 2 caracteres. | | `currency` | texto | Sí | Moneda de la cuenta, en código de 3 letras. 3 caracteres. | | `document` | texto | No | Documento del titular, si el país lo pide. Hasta 30 caracteres. | | `document_type` | texto | No | Tipo de documento del titular, si el país lo pide. [`GET /v1/latam/countries`](/docs/operaciones/listar-paises-latam/) dice qué pide cada país. Hasta 20 caracteres. | | `entity` | texto | No | Banco o entidad, si el país lo pide. Los valores salen de [`GET /v1/latam/countries/{country}/entities`](/docs/operaciones/listar-bancos-latam/). Hasta 40 caracteres. | | `holder_name` | texto | Sí | Titular de la cuenta destino. Hasta 140 caracteres. | | `rail` | texto | Sí | Riel de pago. Decide qué campos se piden. `latam`: Transferencia local en otros países de América Latina. | ```json { "rail": "latam", "holder_name": "PROVEEDOR FICTICIO SPA", "country": "CL", "currency": "CLP", "account_number": "00000000", "entity": "000", "document_type": "RUT", "document": "00000000-0" } ``` Para Perú, con `account_type` y sin `entity`: ```json { "rail": "latam", "holder_name": "PROVEEDOR FICTICIO SAC", "country": "PE", "currency": "PEN", "account_number": "00000000000000000000", "account_type": "savings", "document_type": "RUC", "document": "00000000000" } ``` Un alta `latam` con datos inválidos responde `422 invalid_beneficiary` al momento, sin dejar beneficiario. Corrige los datos y usa una clave nueva: la misma repite ese rechazo. `202 processing` significa que el alta aún no se confirma, por un corte o una falla temporal, no que los datos estén mal: repite con la misma clave. ## Paga Aprueba el beneficiario en el panel y crea el pago con [`POST /v1/payments`](/docs/operaciones/crear-pago/), en la moneda de la cuenta. `USD` no aplica en este riel y responde `422 currency_not_supported`. - `CLP` no lleva decimales: `1500.50` responde `400 invalid_request`. - El débito se calcula al crear el pago. Para fijar un tope, cotiza con [`POST /v1/quotes`](/docs/operaciones/crear-cotizacion/). - `completed` exige la confirmación del banco de destino: mientras no llegue, el pago sigue en `processing`. Ver [Qué confirma un `completed`](/docs/conceptos/#qué-confirma-un-completed). - Un pago `latam` no se cancela: [`POST /v1/payments/{id}/cancel`](/docs/operaciones/cancelar-pago/) responde `409 not_cancellable`. ## Errores comunes - `403 not_enabled`: el riel no está activado para tu empresa. - `422 amount_below_minimum`: el monto queda por debajo del mínimo aplicable. Usa una clave nueva. - `422 rail_unavailable`: el país no está disponible por ahora. Repite más tarde con la misma clave. ## Qué sigue [Pago masivo](/docs/recetas/pago-masivo/). --- # Pago masivo Dispersar un lote con idempotencia y saldo. La API no tiene un endpoint de lote: envías un pago por línea, una salida a la vez por empresa, con una clave derivada de cada línea. ## Pasos 1. Verifica saldo con [`GET /v1/balances`](/docs/operaciones/listar-saldos/). 2. Para cada línea, calcula la clave: `lote-2026-11-pago-0001`. 3. Crea el pago. Si responde `409 payment_in_flight`, espera y repite con la misma clave. 4. Guarda el `pay_` junto a tu línea. 5. Sigue los estados por webhook o con [`GET /v1/payments`](/docs/operaciones/listar-pagos/). Si tu proceso se cae a la mitad, vuelve a correr todo el lote: las líneas ya enviadas devuelven su respuesta original con `Idempotent-Replayed: true`. ## Ritmo Respeta 5 escrituras por segundo por llave. Sólo hay una salida de dinero en vuelo por empresa, así que paralelizar no acelera. ## Errores comunes - Claves generadas al azar: un reinicio paga dos veces. - Detener el lote por un `422` en una línea. Regístralo y sigue. ## Qué sigue [Conciliar](/docs/recetas/conciliar/). --- # Pago PIX Pagar por PIX si tu empresa ya lo tiene habilitado. PIX funciona igual que cualquier pago, pero sólo si tu empresa ya lo tiene activo. Si no, la respuesta es `403 not_enabled`. ## Comprobar si lo tienes [`GET /v1/limits`](/docs/operaciones/consultar-limites/) lista tus rieles en `rails`. [`GET /v1/status`](/docs/operaciones/consultar-estado/) muestra `payments_pix` como `not_enabled` si no aplica. ## Registrar el beneficiario | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `document` | texto | No | Documento del titular. Hasta 20 caracteres. | | `holder_name` | texto | Sí | Titular de la cuenta destino. Hasta 140 caracteres. | | `pix_key` | texto | Sí | Llave PIX del titular. Hasta 100 caracteres. | | `rail` | texto | Sí | Riel de pago. Decide qué campos se piden. `pix`: PIX, en Brasil. | ```json { "rail": "pix", "holder_name": "PROVEEDOR FICTICIO LTDA", "pix_key": "llave-pix-de-ejemplo", "document": "00000000000" } ``` `document` es opcional. Si lo envías, se conservan sólo sus dígitos. El alta no pide activación, el pago sí. Después sigue el mismo flujo de [Pago SPEI](/docs/recetas/pago-spei/), con `currency` en `BRL`: aprobación en el panel, [`POST /v1/payments`](/docs/operaciones/crear-pago/), estado y recibo. ## Qué significa `not_enabled` La función no está activa para tu empresa. No se reintenta. La activación no se hace por API: consulta con tu ejecutivo FITAL. Practícalo en el simulador con `X-Fital-Scenario: pix_not_enabled`. ## Errores comunes - Reintentar en bucle un `not_enabled`. - Confundirlo con `rail_unavailable`, que es temporal. ## Qué sigue [Pago LatAm](/docs/recetas/pago-latam/). --- # Pago SEPA Pagar en euros a una cuenta de la zona euro. Para pagar por SEPA registras el IBAN y el BIC de la cuenta, la apruebas en el panel y creas el pago en `EUR`. ## Requisitos | Requisito | Valor | |---|---| | `rail` | `eur_sepa` | | País | Zona euro | | Moneda del pago | `EUR` | | Datos de la cuenta | IBAN y BIC | | Disponible si | `eur_sepa` aparece en `rails` de [`GET /v1/limits`](/docs/operaciones/consultar-limites/). Si no, `403 rail_not_enabled` | | Cuenta de prueba | No se simula: responde `403 rail_not_enabled` | ## Registra el beneficiario | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `bic` | texto | Sí | Código BIC del banco. De 8 a 11 caracteres. | | `holder_name` | texto | Sí | Titular de la cuenta destino. Hasta 140 caracteres. | | `iban` | texto | Sí | IBAN de la cuenta. De 15 a 34 caracteres. | | `iban_country` | texto | Sí | País del IBAN, en código de 3 letras. 3 caracteres. | | `rail` | texto | Sí | Riel de pago. Decide qué campos se piden. `eur_sepa`: SEPA, en la zona euro. | ```json { "rail": "eur_sepa", "holder_name": "PROVEEDOR FICTICIO SL", "iban": "XX00FICTICIO0000000000", "bic": "FICTXXXX", "iban_country": "ESP" } ``` Los espacios del IBAN se quitan, e IBAN y BIC se guardan en mayúsculas. `iban_country` va en código de 3 letras, como `ESP`. ## Paga Aprueba el beneficiario en el panel y crea el pago con [`POST /v1/payments`](/docs/operaciones/crear-pago/), con `currency` en `EUR`. El resto del flujo es igual al de [Pago SPEI](/docs/recetas/pago-spei/). ## Errores comunes - `400 invalid_request`: `iban_country` lleva 3 letras, no 2. - `422 invalid_beneficiary`: el riel rechazó la cuenta. Revisa IBAN y BIC. - `422 currency_not_supported`: el pago no va en `EUR`. ## Qué sigue [Pago FPS](/docs/recetas/pago-fps/). --- # Pago SPEI Dispersar en México, desde el alta del beneficiario hasta el recibo. Para pagar por SPEI registras una CLABE, la apruebas en el panel y creas el pago. Los pasos siguientes usan el simulador público. ## Requisitos | Requisito | Valor | |---|---| | `rail` | `mxn_spei` | | País | México | | Moneda del pago | `MXN` | | Datos de la cuenta | CLABE | | Disponible si | `mxn_spei` aparece en `rails` de [`GET /v1/limits`](/docs/operaciones/consultar-limites/). Si no, `403 rail_not_enabled` | | Cuenta de prueba | Se simula, con CLABE `999` y titular con `FICTICIO` | ## 1. Registra el beneficiario Con [`POST /v1/beneficiaries`](/docs/operaciones/crear-beneficiario/). La CLABE lleva exactamente 18 dígitos, sin espacios. **Variables de estas muestras: simulador público, sin cuenta** Define esto en tu terminal. Con estos valores las muestras corren tal cual contra el simulador público: ```bash export FITAL_API="https://widget.fital.xyz/explorar/v1" export FITAL_KEY="sk_test_sandbox_publica" export FITAL_SIGNING_SECRET="secreto_de_firma_de_prueba_publico" ``` En la cuenta de prueba o en producción cambia la base, la llave y el secreto, y sustituye los `id` de ejemplo por los tuyos. Ver [Ambientes](/docs/ambientes/). ```bash CUERPO='{"rail":"mxn_spei","holder_name":"PROVEEDOR FICTICIO SA DE CV","clabe":"999180000000000011"}' CLAVE="beneficiario-proveedor-77" TS=$(date +%s) HASH=$(printf '%s' "$CUERPO" | openssl dgst -sha256 -r | cut -d' ' -f1) FIRMA=$(printf '%s\n%s\n%s\n%s\n%s' "$TS" POST /v1/beneficiaries "$HASH" "$CLAVE" \ | openssl dgst -sha256 -hmac "$FITAL_SIGNING_SECRET" -r | cut -d' ' -f1) curl -sS -X POST "$FITAL_API/beneficiaries" \ -H "Authorization: Bearer $FITAL_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $CLAVE" \ -H "X-Fital-Timestamp: $TS" \ -H "X-Fital-Signature: v1=$FIRMA" \ -d "$CUERPO" ``` ```js import { createHash, createHmac } from 'node:crypto'; const cuerpo = JSON.stringify({"rail":"mxn_spei","holder_name":"PROVEEDOR FICTICIO SA DE CV","clabe":"999180000000000011"}); const clave = 'beneficiario-proveedor-77'; const ts = String(Math.floor(Date.now() / 1000)); const hash = createHash('sha256').update(cuerpo).digest('hex'); const firma = createHmac('sha256', process.env.FITAL_SIGNING_SECRET) .update([ts, 'POST', '/v1/beneficiaries', hash, clave].join('\n')).digest('hex'); const r = await fetch(`${process.env.FITAL_API}/beneficiaries`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.FITAL_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': clave, 'X-Fital-Timestamp': ts, 'X-Fital-Signature': `v1=${firma}`, }, body: cuerpo, }); console.log(r.status, await r.json()); ``` ```python import hashlib, hmac, json, os, time, urllib.error, urllib.request datos = {"rail":"mxn_spei","holder_name":"PROVEEDOR FICTICIO SA DE CV","clabe":"999180000000000011"} cuerpo = json.dumps(datos).encode() clave = "beneficiario-proveedor-77" ts = str(int(time.time())) canon = "\n".join([ts, "POST", "/v1/beneficiaries", hashlib.sha256(cuerpo).hexdigest(), clave]) firma = hmac.new(os.environ["FITAL_SIGNING_SECRET"].encode(), canon.encode(), hashlib.sha256).hexdigest() pet = urllib.request.Request(os.environ["FITAL_API"] + "/beneficiaries", method="POST", data=cuerpo, headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"], "Content-Type": "application/json", "Idempotency-Key": clave, "X-Fital-Timestamp": ts, "X-Fital-Signature": "v1=" + firma}) try: with urllib.request.urlopen(pet) as r: print(r.status, json.load(r)) except urllib.error.HTTPError as e: print(e.code, e.read().decode()) ``` ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('FITAL_KEY'), 'Content-Type: application/json', 'Idempotency-Key: ' . $clave, 'X-Fital-Timestamp: ' . $ts, 'X-Fital-Signature: v1=' . $firma, ], CURLOPT_POSTFIELDS => $cuerpo, ]); echo curl_exec($c), "\n"; ``` Nace pendiente. Apruébalo en el panel: por API no se puede. ## 2. Cotiza, si te importa el débito [`POST /v1/quotes`](/docs/operaciones/crear-cotizacion/) con `beneficiary_id`, `amount` y `currency`. Envía el `quote_id` al crear el pago y el débito no superará lo cotizado. ## 3. Crea el pago Con [`POST /v1/payments`](/docs/operaciones/crear-pago/): ```bash CUERPO='{"beneficiary_id":"ben_3kTMd9wQp2LxZ7vR1nYcA4bE","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"}' CLAVE="pago-factura-1042" TS=$(date +%s) HASH=$(printf '%s' "$CUERPO" | openssl dgst -sha256 -r | cut -d' ' -f1) FIRMA=$(printf '%s\n%s\n%s\n%s\n%s' "$TS" POST /v1/payments "$HASH" "$CLAVE" \ | openssl dgst -sha256 -hmac "$FITAL_SIGNING_SECRET" -r | cut -d' ' -f1) curl -sS -X POST "$FITAL_API/payments" \ -H "Authorization: Bearer $FITAL_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $CLAVE" \ -H "X-Fital-Timestamp: $TS" \ -H "X-Fital-Signature: v1=$FIRMA" \ -d "$CUERPO" ``` ```js import { createHash, createHmac } from 'node:crypto'; const cuerpo = JSON.stringify({"beneficiary_id":"ben_3kTMd9wQp2LxZ7vR1nYcA4bE","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"}); const clave = 'pago-factura-1042'; const ts = String(Math.floor(Date.now() / 1000)); const hash = createHash('sha256').update(cuerpo).digest('hex'); const firma = createHmac('sha256', process.env.FITAL_SIGNING_SECRET) .update([ts, 'POST', '/v1/payments', hash, clave].join('\n')).digest('hex'); const r = await fetch(`${process.env.FITAL_API}/payments`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.FITAL_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': clave, 'X-Fital-Timestamp': ts, 'X-Fital-Signature': `v1=${firma}`, }, body: cuerpo, }); console.log(r.status, await r.json()); ``` ```python import hashlib, hmac, json, os, time, urllib.error, urllib.request datos = {"beneficiary_id":"ben_3kTMd9wQp2LxZ7vR1nYcA4bE","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"} cuerpo = json.dumps(datos).encode() clave = "pago-factura-1042" ts = str(int(time.time())) canon = "\n".join([ts, "POST", "/v1/payments", hashlib.sha256(cuerpo).hexdigest(), clave]) firma = hmac.new(os.environ["FITAL_SIGNING_SECRET"].encode(), canon.encode(), hashlib.sha256).hexdigest() pet = urllib.request.Request(os.environ["FITAL_API"] + "/payments", method="POST", data=cuerpo, headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"], "Content-Type": "application/json", "Idempotency-Key": clave, "X-Fital-Timestamp": ts, "X-Fital-Signature": "v1=" + firma}) try: with urllib.request.urlopen(pet) as r: print(r.status, json.load(r)) except urllib.error.HTTPError as e: print(e.code, e.read().decode()) ``` ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('FITAL_KEY'), 'Content-Type: application/json', 'Idempotency-Key: ' . $clave, 'X-Fital-Timestamp: ' . $ts, 'X-Fital-Signature: v1=' . $firma, ], CURLOPT_POSTFIELDS => $cuerpo, ]); echo curl_exec($c), "\n"; ``` ## 4. Sigue el estado ```bash curl -sS "$FITAL_API/payments/pay_6nRtY1wKs3QzD8cL5mVhF2gB" \ -H "Authorization: Bearer $FITAL_KEY" \ -H "X-Fital-Scenario: payment_completed" ``` ```js const r = await fetch(`${process.env.FITAL_API}/payments/pay_6nRtY1wKs3QzD8cL5mVhF2gB`, { headers: { Authorization: `Bearer ${process.env.FITAL_KEY}`, 'X-Fital-Scenario': 'payment_completed' }, }); console.log(r.status, await r.json()); ``` ```python import json, os, urllib.error, urllib.request pet = urllib.request.Request(os.environ["FITAL_API"] + "/payments/pay_6nRtY1wKs3QzD8cL5mVhF2gB", headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"], "X-Fital-Scenario": "payment_completed"}) try: with urllib.request.urlopen(pet) as r: print(r.status, json.load(r)) except urllib.error.HTTPError as e: print(e.code, e.read().decode()) ``` ```php true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('FITAL_KEY'), 'X-Fital-Scenario: payment_completed'], ]); echo curl_exec($c), "\n"; ``` El detalle trae `freshness`. Con `delayed`, `status` es lo último que se sabe. Ver [Frescura del detalle](/docs/conceptos/#frescura-del-detalle). ## 5. Cancela si aún se puede [`POST /v1/payments/{id}/cancel`](/docs/operaciones/cancelar-pago/) sólo funciona mientras el pago espera fondos. `409 not_cancellable` es la respuesta normal cuando ya avanzó. Tras cancelar, el detalle puede responder `canceled` con `freshness.status` en `delayed` hasta que una lectura lo confirme, y [`GET /v1/payments`](/docs/operaciones/listar-pagos/) puede tardar una vuelta de sincronización en reflejarlo. ## Errores comunes - `422 beneficiary_not_approved`: apruébalo y repite con la misma clave. - `422 insufficient_funds`: fondea y repite con una clave nueva. La misma clave repite el rechazo. - `403 rail_not_enabled`: el riel no está habilitado para tu cuenta. ## Qué sigue [Pago ACH](/docs/recetas/pago-ach/) y [Movimientos y recibos](/docs/recetas/movimientos-y-recibos/). --- # Pago wire Pagar en dólares a una cuenta de Estados Unidos por wire. Un pago wire pide los mismos datos que uno ACH: lo que cambia es `rail`. Cada uno es un riel aparte, con su propia habilitación. ## Requisitos | Requisito | Valor | |---|---| | `rail` | `usd_wire` | | País | Estados Unidos | | Moneda del pago | `USD` | | Datos de la cuenta | Número de ruta, cuenta y domicilio | | Disponible si | `usd_wire` aparece en `rails` de [`GET /v1/limits`](/docs/operaciones/consultar-limites/). Si no, `403 rail_not_enabled` | | Cuenta de prueba | No se simula: responde `403 rail_not_enabled` | ## Registra el beneficiario Los campos y sus reglas son los de [Pago ACH](/docs/recetas/pago-ach/#registra-el-beneficiario), con `rail: "usd_wire"`: ```json { "rail": "usd_wire", "holder_name": "PROVEEDOR FICTICIO LLC", "routing_number": "999999999", "account_number": "000123456789", "account_type": "checking", "address": { "street_line_1": "100 Calle Ficticia", "city": "Ciudad Ficticia", "state": "TX", "postal_code": "00000", "country": "USA" } } ``` Si ya registraste esa cuenta con `usd_ach`, regístrala otra vez con `usd_wire` para pagarle por wire: el riel queda fijo en cada beneficiario. ## Paga Aprueba el beneficiario en el panel y crea el pago con [`POST /v1/payments`](/docs/operaciones/crear-pago/), con `currency` en `USD`. El resto del flujo es igual al de [Pago SPEI](/docs/recetas/pago-spei/). ## Errores comunes - `403 rail_not_enabled`: tu empresa puede tener `usd_ach` sin `usd_wire`. Revisa `rails` en [`GET /v1/limits`](/docs/operaciones/consultar-limites/). - `400 invalid_request`: el domicilio sigue las mismas reglas que en ACH. ## Qué sigue [Pago SEPA](/docs/recetas/pago-sepa/). --- # Quién pagó Consulta los depósitos recibidos y los datos del pagador. [`GET /v1/deposits`](/docs/operaciones/listar-depositos/) lista el dinero recibido con los datos del pagador cuando existen. El pagador es opcional: depende de lo que informe el riel. Nunca se inventa. **Variables de estas muestras: simulador público, sin cuenta** Define esto en tu terminal. Con estos valores las muestras corren tal cual contra el simulador público: ```bash export FITAL_API="https://widget.fital.xyz/explorar/v1" export FITAL_KEY="sk_test_sandbox_publica" ``` En la cuenta de prueba o en producción cambia la base, la llave y el secreto, y sustituye los `id` de ejemplo por los tuyos. Ver [Ambientes](/docs/ambientes/). ```bash curl -sS "$FITAL_API/deposits?limit=10" \ -H "Authorization: Bearer $FITAL_KEY" \ -H "Accept: application/json" ``` ```js const r = await fetch(`${process.env.FITAL_API}/deposits?limit=10`, { headers: { Authorization: `Bearer ${process.env.FITAL_KEY}`, 'Accept': 'application/json' }, }); console.log(r.status, await r.json()); ``` ```python import json, os, urllib.error, urllib.request pet = urllib.request.Request(os.environ["FITAL_API"] + "/deposits?limit=10", headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"], "Accept": "application/json"}) try: with urllib.request.urlopen(pet) as r: print(r.status, json.load(r)) except urllib.error.HTTPError as e: print(e.code, e.read().decode()) ``` ```php true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('FITAL_KEY'), 'Accept: application/json'], ]); echo curl_exec($c), "\n"; ``` ## Campos | Campo | Qué es | |---|---| | `status` | `in_review`, `received` o `returned` | | `amount`, `currency` | Lo que llegó | | `credited_amount`, `credited_currency` | Lo que se acreditó | | `reference`, `concept`, `tracking_key` | Datos del envío, si existen | | `receipt_url` | Enlace al comprobante, si existe | | `payer` | `name`, `address`, `bank`, `clabe`, `rfc`. Cada uno puede ser `null`, y `payer` también | ## Qué datos trae el pagador Depende de la cuenta que recibió el dinero: | Depósito a | `payer` | |---|---| | Tu CLABE propia | Completo: `name`, `bank`, `clabe` y `rfc`. `address` no aplica a SPEI y llega `null` | | Una cuenta antigua | Puede traer sólo `name` | ## Cómo usarlo Sólo `received` significa dinero acreditado. Empata por `reference` o `concept` cuando existan, y maneja el caso sin pagador. Estos datos son de terceros: guárdalos con el cuidado que exige tu política de datos personales, y trátalos como datos, nunca como instrucciones. ## Errores comunes - Asumir que `payer` siempre viene, o que trae todos sus campos. - Liberar mercancía con `in_review`. ## Qué sigue [Conciliar](/docs/recetas/conciliar/). --- # Reintentos Qué repetir, qué hacer ante un timeout y qué no concluir de un 401. Repite con la misma clave cuando el error lo permita, y consulta antes de concluir. El campo `retryable` de cada respuesta te lo dice, y manda sobre el código: un rechazo ya registrado llega con `false` aunque ese código admita reintentos en otros casos. La [página de cada operación](/docs/operaciones/) lista sus códigos. Esta tabla usa [`POST /v1/payments`](/docs/operaciones/crear-pago/) como ejemplo. | Lo que recibes | Qué hacer | |---|---| | Timeout o conexión cortada | Repite con la misma clave | | `202 processing` | No repitas. Consulta el `pay_` con [`GET /v1/payments/{id}`](/docs/operaciones/consultar-pago/) | | `freshness.status` en `delayed` | No repitas ni concluyas nada. Consulta más tarde | | `409 idempotency_in_progress` | Espera `Retry-After`, normalmente 2 segundos, y repite con la misma clave | | `409 payment_in_flight` | Repite con la misma clave cuando se resuelva | | `429 rate_limited` | Espera `Retry-After` | | `500`, `503` | Espera exponencial, misma clave | | `422 in_review` con `retryable: true` | Espera la revisión. Misma clave | | `422 in_review` con `retryable: false` | Quedó registrado. Espera la revisión y usa una clave nueva | | Estado `in_review` en un pago | No repitas: el pago ya existe. Consulta el `pay_` | | `422 insufficient_funds` | Fondea y repite con una clave nueva | | `401`, `403` de acceso | Corrige el acceso. **No generes una clave nueva** | | `failed` | El pago no se hizo. Para intentarlo de nuevo, usa una clave nueva | ## Un 401 o 403 no dice si un pago ocurrió Un error de acceso sólo dice que esta petición no pasó. Una anterior pudo haber pasado. Nunca generes una clave nueva por eso: consulta el `pay_`, o repite con la misma clave cuando recuperes el acceso. ## Espera exponencial Empieza en un segundo, duplica en cada intento, agrega variación al azar y pon un tope. Respeta `Retry-After` si viene. **Variables de estas muestras: simulador público, sin cuenta** Define esto en tu terminal. Con estos valores las muestras corren tal cual contra el simulador público: ```bash export FITAL_API="https://widget.fital.xyz/explorar/v1" export FITAL_KEY="sk_test_sandbox_publica" export FITAL_SIGNING_SECRET="secreto_de_firma_de_prueba_publico" ``` En la cuenta de prueba o en producción cambia la base, la llave y el secreto, y sustituye los `id` de ejemplo por los tuyos. Ver [Ambientes](/docs/ambientes/). ```bash CUERPO='{"beneficiary_id":"ben_3kTMd9wQp2LxZ7vR1nYcA4bE","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"}' CLAVE="pago-factura-1042" TS=$(date +%s) HASH=$(printf '%s' "$CUERPO" | openssl dgst -sha256 -r | cut -d' ' -f1) FIRMA=$(printf '%s\n%s\n%s\n%s\n%s' "$TS" POST /v1/payments "$HASH" "$CLAVE" \ | openssl dgst -sha256 -hmac "$FITAL_SIGNING_SECRET" -r | cut -d' ' -f1) curl -sS -X POST "$FITAL_API/payments" \ -H "Authorization: Bearer $FITAL_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $CLAVE" \ -H "X-Fital-Timestamp: $TS" \ -H "X-Fital-Signature: v1=$FIRMA" \ -d "$CUERPO" ``` ```js import { createHash, createHmac } from 'node:crypto'; const cuerpo = JSON.stringify({"beneficiary_id":"ben_3kTMd9wQp2LxZ7vR1nYcA4bE","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"}); const clave = 'pago-factura-1042'; const ts = String(Math.floor(Date.now() / 1000)); const hash = createHash('sha256').update(cuerpo).digest('hex'); const firma = createHmac('sha256', process.env.FITAL_SIGNING_SECRET) .update([ts, 'POST', '/v1/payments', hash, clave].join('\n')).digest('hex'); const r = await fetch(`${process.env.FITAL_API}/payments`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.FITAL_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': clave, 'X-Fital-Timestamp': ts, 'X-Fital-Signature': `v1=${firma}`, }, body: cuerpo, }); console.log(r.status, await r.json()); ``` ```python import hashlib, hmac, json, os, time, urllib.error, urllib.request datos = {"beneficiary_id":"ben_3kTMd9wQp2LxZ7vR1nYcA4bE","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"} cuerpo = json.dumps(datos).encode() clave = "pago-factura-1042" ts = str(int(time.time())) canon = "\n".join([ts, "POST", "/v1/payments", hashlib.sha256(cuerpo).hexdigest(), clave]) firma = hmac.new(os.environ["FITAL_SIGNING_SECRET"].encode(), canon.encode(), hashlib.sha256).hexdigest() pet = urllib.request.Request(os.environ["FITAL_API"] + "/payments", method="POST", data=cuerpo, headers={"Authorization": "Bearer " + os.environ["FITAL_KEY"], "Content-Type": "application/json", "Idempotency-Key": clave, "X-Fital-Timestamp": ts, "X-Fital-Signature": "v1=" + firma}) try: with urllib.request.urlopen(pet) as r: print(r.status, json.load(r)) except urllib.error.HTTPError as e: print(e.code, e.read().decode()) ``` ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('FITAL_KEY'), 'Content-Type: application/json', 'Idempotency-Key: ' . $clave, 'X-Fital-Timestamp: ' . $ts, 'X-Fital-Signature: v1=' . $firma, ], CURLOPT_POSTFIELDS => $cuerpo, ]); echo curl_exec($c), "\n"; ``` ## Errores comunes - Marcar como fallido un pago por un timeout. - Reintentar sin tope. - Decidir por el código sin mirar `retryable`: `in_review` puede llegar con los dos valores. ## Qué sigue [Límites](/docs/limites/). --- # Lista para producción Lo que debes cumplir antes de mover dinero real. Cuando todas las casillas estén marcadas, tu integración puede salir a producción. Cambiar de ambiente sólo cambia la base y la llave. - [ ] KYB aprobado. [Conceptos](/docs/conceptos/). Escenario: `kyb_required`. - [ ] Contrato firmado, con la cláusula de responsabilidad por montos. [Límites](/docs/limites/#montos). - [ ] Giro mercantil, representante legal e identificación entregados a FITAL. [Soporte](/docs/soporte/). - [ ] Ciclo completo probado en la cuenta de prueba: pago, fallo, devolución. [Cuenta de prueba](/docs/sandbox/cuenta-de-prueba/). Escenarios: `payment_completed`, `payment_failed`, `payment_returned`. - [ ] Reintento con misma clave probado ante timeout. [Reintentos](/docs/recetas/reintentos/). - [ ] 429 y 503 manejados con espera. [Límites](/docs/limites/). Escenarios: `rate_limited`, `service_unavailable`. - [ ] Firma de webhook verificada, con rechazo de firma mala y de timestamp vencido. [Verificar la firma](/docs/webhooks/verificar-firma/). - [ ] Duplicados y desorden de eventos resueltos. [Webhooks](/docs/webhooks/#duplicados-y-desorden). - [ ] Manejo de `returned` después de `completed` y de un pago que se reabre. [Conciliar](/docs/recetas/conciliar/). Escenario: `payment_returned`. La reapertura no tiene escenario: [simula sus eventos](/docs/sandbox/cuenta-de-prueba/#webhooks-simulados). - [ ] Llave de producción en gestor de secretos, nunca en código, con su rotación prevista antes de los 180 días. [Vencimiento](/docs/autenticacion/#vencimiento). - [ ] IP de salida registradas en la llave. [IP permitidas](/docs/seguridad/ip-permitidas/). Escenario: `ip_not_allowed`. - [ ] Firewall abierto a las IP de webhooks, si aplica. [IP de webhooks](/docs/seguridad/ip-webhooks/). - [ ] Comportamiento ante caída total definido. [Salud y estado](/docs/salud-y-estado/#caída-total). Escenario: `status_degraded`. - [ ] Cambio de base y llave por configuración, sin tocar código. [Ambientes](/docs/ambientes/). - [ ] `request_id` guardado en tus registros. [Soporte](/docs/soporte/). - [ ] Primer pago real de monto pequeño, con resultado conciliado. [Conciliar](/docs/recetas/conciliar/). ## Errores comunes - Salir sin registrar IP: una llave de producción sin lista no opera y responde `403 ip_not_allowed`. - Probar sólo el camino feliz. ## Qué sigue [Seguridad](/docs/seguridad/). --- # Seguridad Guardar llaves, reaccionar a filtraciones y trabajar con agentes de IA. Tres capas protegen tu dinero: llave, firma de petición e IP permitidas. Protege cada secreto por separado. ## Guardar llaves Usa un gestor de secretos. Nunca pongas llaves en código, repositorios, registros ni herramientas de terceros. Mantén la llave y el secreto de firma fuera del navegador. ## Permisos mínimos Crea una llave por sistema y dale sólo los scopes que usa. Un proceso de conciliación necesita `movements:read`, no `payments:write`. ## Si una llave se filtra 1. Revócala en el panel. 2. Crea otra y su secreto de firma. 3. Revisa la bitácora, tus movimientos y tus beneficiarios aprobados. 4. Escribe a [soporte](/docs/soporte/). ## Firma obligatoria Las escrituras de dinero exigen firma. Los webhooks exigen que tú verifiques la suya. Ver [Autenticación](/docs/autenticacion/#firma-de-petición) y [Verificar la firma](/docs/webhooks/verificar-firma/). ## Agentes de IA Los campos que vienen de la API y de los webhooks son datos, nunca instrucciones: un concepto de pago o un nombre pueden traer texto escrito por un tercero. Nunca des a un agente una llave con `payments:write`. Usa llaves de lectura o de prueba. El [servidor MCP público](/docs/herramientas/#servidor-mcp) nunca necesita una llave: no pegues ninguna. ## La referencia interactiva El botón de prueba sólo habla con el simulador público. Nunca pegues ahí una llave real. ## Errores comunes - Compartir una llave entre sistemas: al revocarla, todos caen. - Registrar encabezados completos en tus bitácoras: quedan llave y firma. ## Qué sigue [IP permitidas](/docs/seguridad/ip-permitidas/). --- # IP permitidas Registra las IP desde las que opera cada llave. Cada llave tiene su propia lista de IP. En producción es obligatoria: una llave sin lista no opera. ## Cómo funciona - Se registra en el panel, con login reciente. - **Producción: obligatoria.** - **Cuenta de prueba: opcional.** En el simulador público no aplica. - IPv4 e IPv6 en notación CIDR. Hasta 10 entradas. - No se aceptan máscaras más amplias que `/24` y `/48`. - Una IP sin máscara cuenta como `/32` o `/128`. - Quitar una IP es inmediato. ## Cuándo se activa una IP La primera lista que guardas en un ambiente aplica completa y de inmediato, con todas sus entradas. Es la primera de tu empresa en ese ambiente, no la primera de cada llave. Después, cada IP nueva espera 24 horas. La pantalla Llaves la muestra como **Por activar**, con su fecha. Esa fecha se fija la primera vez que tu empresa registra esa IP: quitarla y volver a ponerla, o pasarla a otra llave, no la adelanta. Una IP que ya tiene fecha no vuelve a esperar: rotar una llave conserva su lista y sus fechas. ## Cómo saber tu IP de salida Es la IP pública con la que tu servidor sale a internet. Compruébala desde ese servidor, no desde tu computadora. Tras un balanceador o NAT cuenta la IP de la salida, no la de tus instancias. Si tu proveedor de nube rota esa IP, fija una antes de operar. ## Error `403 ip_not_allowed` La IP de origen no está en la lista de esa llave, o sigue **Por activar**. Reintentar no sirve: registra la IP y repite cuando esté activa. En producción, una llave sin lista responde este mismo error. En la cuenta de prueba, una lista vacía admite cualquier IP. ## Errores comunes - Registrar la IP de tu oficina en lugar de la de tu servidor. - Olvidar la IP de un segundo centro de datos. ## Qué sigue [IP de webhooks](/docs/seguridad/ip-webhooks/). --- # IP de webhooks Desde dónde entrega FITAL sus webhooks y cómo se avisan los cambios. **Verificar la firma HMAC sigue siendo obligatorio.** Una IP correcta no prueba que el mensaje sea de FITAL. Esta lista sirve para abrir tu firewall y no sustituye la firma. > **Próximo.** La lista se publica cuando se fije la IP definitiva de salida. Antes de eso no hay ninguna IP vigente. La lista también está en [`/webhook-ips.json`](https://merchants.fital.xyz/webhook-ips.json) y en [`GET /v1/meta/egress-ips`](/docs/operaciones/consultar-ip-de-salida/). En la cuenta de prueba esa ruta responde una lista vacía. ## Aviso de cambios Una IP nueva se publica 15 días antes de usarse, salvo arreglos urgentes de seguridad, en [Cambios](/docs/cambios/), RSS y panel. Durante la transición se publican ambas. El evento `system.egress_ips.changing` avisa lo mismo a tu destino, con `ip`, `change` (`added` o `retiring`) y `effective_at`. Llega una vez a cada destino activo y suscrito, también si lo activas después del anuncio, hasta una hora antes de `effective_at`. Un destino activado en esa última hora puede no recibir el webhook. El aviso sigue en [`GET /v1/events`](/docs/operaciones/listar-eventos/), y las IP en [`GET /v1/meta/egress-ips`](/docs/operaciones/consultar-ip-de-salida/). Ver [su cuerpo](/docs/webhooks/eventos/#systemegress_ipschanging). ## Errores comunes - Filtrar por IP y omitir la firma. - Fijar la lista en tu firewall sin vigilar [Cambios](/docs/cambios/). ## Qué sigue [Webhooks](/docs/webhooks/). --- # Cambios Cambios de la API por fecha, con su clasificación. Aquí se publica todo cambio del contrato. Suscríbete al [RSS](/docs/cambios/rss.xml). Cada entrada está clasificada como Nuevo, Mejora, Corrección, Obsoleto o Cambio incompatible. ## 2026-11-06 · Corrección Un monto de entrada en cero (`0`, `0.0`, `0.00`) responde `400 invalid_request` en pagos, cotizaciones y cobros, en live y en sandbox. No abre operación ni consume cupo. Si `max_source_amount` o `destination_amount` van, también deben ser mayores que cero. ## 2026-11-06 · Mejora Los pagos tienen un tope acumulado por empresa, por día natural de México y por días corridos. Rebasarlo responde `422 limit_exceeded` con `retryable: false`. Los valores y cómo se cuenta están en [Límites](/docs/limites/#montos). `max_amount` sigue en `null`: no hay tope por pago. ## 2026-11-05 · Mejora `Movement` y `Receipt` traen `payment_id` y `external_ref`: el movimiento dice de qué pago de esta API sale. En webhooks, el código de estado decide: cualquier `2xx` es éxito, y FITAL cierra la conexión al recibirlo. Deduplica por el `id` del cuerpo, que va firmado, y no por `X-Fital-Event-Id`. Para recuperar eventos, pide [`GET /v1/events`](/docs/operaciones/listar-eventos/) sin cursor y recorre hasta uno que ya procesaste. La cuenta de prueba ahora aplica las validaciones documentadas de moneda, pares de depósito, baja y estimación, y un beneficiario ajeno responde `404 not_found`. ## 2026-11-05 · Mejora El documento público declara `Event.type` y `WebhookEndpoint.status` como enumeraciones, `Event.data` con una forma por tipo, la sección `webhooks`, `Retry-After` en `409` y `503`, `413` en cada operación con cuerpo, un ejemplo por código y el significado de cada valor. `system.egress_ips.changing` se emite con `ip`, `change` y `effective_at`. En la cuenta de prueba, [`POST /sandbox/v1/webhooks/trigger`](/docs/operaciones/emitir-evento-de-prueba/) manda la forma real de cada tipo, [`GET /sandbox/v1/latam/countries`](/docs/operaciones/listar-paises-latam/) responde `available: false`, y los escenarios de cobro y depósito dicen su `final`. Los destinos de webhook se gestionan sólo en el panel: `v1` no publica operaciones para eso. ## 2026-11-05 · Mejora Nuevo [`POST /v1/receiving-accounts`](/docs/operaciones/abrir-cuenta-para-recibir/): abre una cuenta para recibir en `USD`, `EUR`, `BRL`, `GBP` o `COP`, si tu empresa tiene esa moneda activada. Hay una por moneda: si ya existe responde `200` con esa misma cuenta y `already_existed: true`. `MXN` responde `422`: tu CLABE ya aparece en [`GET /v1/receiving-accounts`](/docs/operaciones/listar-cuentas-para-recibir/). Pide el permiso nuevo `accounts:write`, firma e `Idempotency-Key`. [`GET /v1/receiving-accounts`](/docs/operaciones/listar-cuentas-para-recibir/) suma `sort_code`, `pix_key`, `br_code` y `bre_b_key`, y ya devuelve el número de cuenta y el BIC de las monedas que antes llegaban vacíos. ## 2026-11-05 · Mejora [`POST /v1/collections`](/docs/operaciones/crear-cobro/) acepta `method: "efectivo"`, bajo solicitud: cobros en tiendas de México, en MXN, de 20.00 a 10,000.00 por operación. La instrucción llega con `type: "REFERENCE"` y `value`, la referencia que quien paga da en la tienda. Se pide con `amount`, no con `destination_amount`, y sin `expires_at` vence a los 3 días. Se consulta en [`GET /v1/collections/{id}`](/docs/operaciones/consultar-cobro/) y avisa con `collection.paid` y `collection.expired`. En `efectivo`, `paid` dice que la tienda confirmó el pago; el abono a tu cuenta llega después. Está disponible bajo solicitud: pídelo a FITAL. Sale en [`GET /v1/collection-methods`](/docs/operaciones/listar-metodos-de-cobro/) sólo cuando tu empresa lo tiene activado. El sandbox lo simula igual, con referencias ficticias. ## 2026-11-05 · Nuevo [`GET /v1/latam/countries`](/docs/operaciones/listar-paises-latam/) y [`GET /v1/latam/countries/{country}/entities`](/docs/operaciones/listar-bancos-latam/) dicen qué países, documentos, reglas de cuenta y bancos admite un alta `latam`. El alta acepta `account_type`, y Perú ya se puede registrar por API. [`GET /v1/collection-methods`](/docs/operaciones/listar-metodos-de-cobro/) añade `payer_fields`, con lo que cada método pide de quien paga; el país de `payer` se deduce de la moneda. Son rutas y campos opcionales nuevos: las integraciones actuales siguen funcionando. Revisa la [lista de migración](/docs/migracion/). ## 2026-11-05 · Corrección [`POST /v1/collections`](/docs/operaciones/crear-cobro/) aplica `payer_fields`, en producción y en la cuenta de prueba. Sin un campo `required`, o con un valor fuera de `options`, responde `400 invalid_request` y nombra el campo en `details.fields`, como `payer.email`, sin crear nada. Ese rechazo no cierra la clave, pero conserva su cuerpo: un cuerpo corregido necesita otra `Idempotency-Key`. Un cobro de prueba por SPEI pide `firstName`, `lastName` y `email`: sin `payer` antes daba `201` y ahora `400`. [`GET /v1/payments/{id}`](/docs/operaciones/consultar-pago/) trae `failure_code` también cuando el detalle ya no se puede leer: un pago `failed` da entonces `payment_rejected`. `system.egress_ips.changing` llega una vez a cada destino, también al que se activa hasta una hora antes del cambio; uno activado en esa última hora puede no recibirlo, y el aviso sigue en [`GET /v1/events`](/docs/operaciones/listar-eventos/). Un pago reenviado cuyo reenvío cae sigue en `processing` hasta que FITAL lo cierre. Revisa la [lista de migración](/docs/migracion/). ## 2026-11-05 · Corrección Un alta `latam` con datos inválidos responde `422 invalid_beneficiary` en vez de quedar en `202 processing`. `failure_code` nunca es `null` en un pago `failed`, y todo evento `payment.*` lo trae. Un rechazo registrado responde `retryable: false`, también con `in_review`. El cuerpo tiene tope de 64 KB y más allá responde `413`. Los webhooks llevan `resource_id`. [`GET /v1/collections/{id}`](/docs/operaciones/consultar-cobro/) devuelve `instruction.type` y `expires_at`. Repetir la clave de [`POST /v1/collections`](/docs/operaciones/crear-cobro/), [`POST /v1/deposit-addresses`](/docs/operaciones/crear-direccion-de-deposito/) o [`POST /v1/beneficiaries`](/docs/operaciones/crear-beneficiario/) tras un `202` entrega el resultado en cuanto existe. En [`GET /v1/movements`](/docs/operaciones/listar-movimientos/) una fila que cambia puede repetirse y nunca se omite otra. Una `Idempotency-Key` con otro cuerpo da `422 idempotency_key_reused` también pasados 7 días. `completed_at` es `null` en un recibo que no terminó en `completed`. Las llaves de producción caducan a 180 días, con avisos a 30, 7 y 1 días. Revisa la [lista de migración](/docs/migracion/). ## 2026-11-05 · Mejora [`POST /v1/deposit-addresses`](/docs/operaciones/crear-direccion-de-deposito/) acepta todas las redes y tokens de [Cobertura](/docs/cobertura/#redes-y-tokens): USDC, USDT, EURC y PYUSD, ahora con `solana`, `tron`, `avalanche`, `celo`, `tempo`, `linea` y `xdc`. `stellar` no está disponible. En `solana`, `tron`, `tempo`, `linea` y `xdc` un depósito rechazado no regresa automáticamente: lo dicen la tabla y el `warning` de la dirección. EURC se acredita en EURC y suma a la fila `EUR` de [`GET /v1/balances`](/docs/operaciones/listar-saldos/). La respuesta trae siempre `chain` en minúsculas y `currency` en mayúsculas. La cuenta de prueba acepta los mismos pares. ## 2026-11-05 · Mejora Nuevo [`GET /v1/wallets`](/docs/operaciones/listar-wallets/): lista las wallets de tu empresa con su red y su saldo disponible, en producción y en la cuenta de prueba. Es sólo consulta: las wallets no se crean ni se modifican por API. Pide el permiso nuevo `wallets:read`, que las llaves existentes no tienen: crea una llave con ese permiso. La suma por moneda es lo que da [`GET /v1/balances`](/docs/operaciones/listar-saldos/), salvo redondeo al centavo. ## 2026-11-04 · Mejora [`GET /v1/payments/{id}`](/docs/operaciones/consultar-pago/) añade `freshness`, con `status` (`current` o `delayed`) y `as_of`. Con `delayed` el estado es lo último que se sabe. Una cancelación confirmada responde `canceled` con `delayed` hasta que una lectura la confirme; un pago cancelado que vuelve a `processing` con `current` se reabrió de verdad. Es un campo opcional nuevo: las integraciones actuales siguen funcionando. ## 2026-11-04 · Nuevo Primera versión pública del contrato `v1`: cuenta, beneficiarios, pagos, cobros, direcciones de depósito, depósitos, movimientos, recibos y eventos. ## Errores comunes - Vigilar sólo esta página. El RSS y el panel avisan lo mismo. ## Qué sigue [Versionado](/docs/versionado/). --- # Lista de migración Qué revisar en tu integración tras los ajustes del contrato v1. Si integraste antes del 5 de noviembre de 2026, revisa esta lista una vez. Ninguna fila quita un campo ni una ruta, pero varias cambian una respuesta que tu código pudo haber asumido. El detalle de cada cambio está en [Cambios](/docs/cambios/). ## Beneficiarios y `latam` | Antes | Ahora | Qué hacer | |---|---|---| | Un alta `latam` inválida quedaba en `202 processing` | Responde `422 invalid_beneficiary` | Maneja ese `422`, corrige los datos y usa una clave nueva. Ver [Pago LatAm](/docs/recetas/pago-latam/) | | El alta `latam` no recibía tipo de cuenta, y Perú no se podía registrar | Acepta `account_type`, y Perú lo exige | Envíalo cuando `account_type_required` sea `true` | | Países y bancos se pedían a tu ejecutivo | [`GET /v1/latam/countries`](/docs/operaciones/listar-paises-latam/) y [`GET /v1/latam/countries/{country}/entities`](/docs/operaciones/listar-bancos-latam/) | Consúltalos en vez de fijarlos en tu código | ## Pagos y errores | Antes | Ahora | Qué hacer | |---|---|---| | `failure_code` podía ser `null` en un pago `failed` | Nunca es `null` en un pago `failed`, tampoco si el detalle ya no se puede leer | Quita la rama de `failed` sin código. Sigue tolerando códigos nuevos | | Los eventos `payment.*` no traían `failure_code` | Todos lo traen, con valor sólo en `payment.failed` | Léelo del evento si te sirve | | Un rechazo registrado podía decir `retryable: true` | Siempre dice `retryable: false`, también con `in_review` | Decide por `retryable` de la respuesta, no por el código. Ver [Qué hacer con la clave tras un rechazo](/docs/idempotencia/#qué-hacer-con-la-clave-tras-un-rechazo) | | El tope del cuerpo no estaba declarado | `413` a partir de 64 KB, con el código `invalid_request` | Parte los lotes grandes. Repetir no sirve. Ver [Límites](/docs/limites/#cuerpo) | | Una `Idempotency-Key` con otro cuerpo dejaba de rechazarse a los 7 días | `422 idempotency_key_reused` también después | No reutilices claves | ## Cobros y altas con `202` | Antes | Ahora | Qué hacer | |---|---|---| | [`POST /v1/collections`](/docs/operaciones/crear-cobro/) no comprobaba `payer`: en la cuenta de prueba, un cobro por SPEI sin `payer` daba `201` | Sin un `required`, o fuera de `options`, responde `400 invalid_request` con `payer.` en `details.fields`. La prueba por SPEI pide `firstName`, `lastName` y `email` | Envía `payer` según `payer_fields`. El cuerpo corregido necesita otra `Idempotency-Key` | | Las claves de `payer` se pedían a tu ejecutivo | `payer_fields` de [`GET /v1/collection-methods`](/docs/operaciones/listar-metodos-de-cobro/) las da por método | Arma `payer` desde esa lista. Ver [Cobros](/docs/recetas/cobros/) | | [`GET /v1/collections/{id}`](/docs/operaciones/consultar-cobro/) no daba `instruction.type` ni `expires_at` | Los da | Quita la copia que guardabas del alta | | Repetir la clave tras un `202` de [`POST /v1/collections`](/docs/operaciones/crear-cobro/), [`POST /v1/deposit-addresses`](/docs/operaciones/crear-direccion-de-deposito/) o [`POST /v1/beneficiaries`](/docs/operaciones/crear-beneficiario/) repetía ese `202` | Entrega el resultado en cuanto existe | Repite con la misma clave hasta tener el resultado | ## Webhooks y eventos | Antes | Ahora | Qué hacer | |---|---|---| | Los webhooks no traían `resource_id` | Lo traen, igual que [`GET /v1/events`](/docs/operaciones/listar-eventos/) | Úsalo para agrupar por recurso | | `Event.type`, `Event.data` y `WebhookEndpoint.status` eran abiertos en el contrato | Son enumeraciones y formas declaradas | Regenera tu cliente. Sigue ignorando los tipos que no conozcas | | `system.egress_ips.changing` no se emitía | Trae `ip`, `change`, `effective_at` y un `resource_id` con `egr_`. Llega una vez por destino, también al que se activa hasta una hora antes del cambio | Vigílalo si filtras por IP. Ver [IP de webhooks](/docs/seguridad/ip-webhooks/) | | No había una respuesta sobre gestionar destinos por API | `v1` sólo los consulta, por decisión: todo cambio va por el panel | No esperes esas operaciones. Ver [Dar de alta un destino](/docs/webhooks/#dar-de-alta-un-destino) | ## Movimientos, recibos y llaves | Antes | Ahora | Qué hacer | |---|---|---| | Una fila de [`GET /v1/movements`](/docs/operaciones/listar-movimientos/) que cambiaba podía perderse | Puede repetirse, y nunca se omite otra | Deduplica por `id` y guarda `next_cursor` en cada página. Ver [Paginación](/docs/paginacion/#movimientos-un-cursor-que-no-termina) | | `completed_at` podía traer fecha en un recibo no completado | `null` si el movimiento no terminó en `completed` | No lo uses para fechar un pago devuelto | | El calendario de avisos de caducidad no estaba publicado | Producción caduca a 180 días, y el panel avisa a 30, 7 y 1 días | Prevé la rotación. Ver [Vencimiento](/docs/autenticacion/#vencimiento) | ## Cuenta de prueba | Antes | Ahora | Qué hacer | |---|---|---| | [`POST /sandbox/v1/webhooks/trigger`](/docs/operaciones/emitir-evento-de-prueba/) mandaba un `data` reducido | Manda la forma real de cada tipo, con datos ficticios y `simulated: true` | Prueba tu receptor con todos los tipos | | No había catálogo `latam` | [`GET /sandbox/v1/latam/countries`](/docs/operaciones/listar-paises-latam/) da países ficticios con `available: false` | No esperes un alta `latam` aquí: responde `403 rail_not_enabled` | ## Errores comunes - Tratar un `202` de alta como un fallo y cambiar de clave. - Copiar la tabla de países en tu código en vez de consultar la API. - Corregir `payer` y repetir con la misma clave. ## Qué sigue [Cambios](/docs/cambios/) y [Versionado](/docs/versionado/). --- # Soporte Cómo contactar a FITAL y qué enviar. Escribe a [soporte@fitalmx.com](mailto:soporte@fitalmx.com). Este canal no depende de la API: funciona aunque FITAL esté caído. ## Qué enviar - El `request_id` de la respuesta, que también llega en `X-Request-Id`. - La hora en UTC, la operación y el ambiente. - El identificador del recurso, como `pay_…`, y tu `external_ref`. Nunca envíes llaves, secretos de firma ni datos completos de tarjetas o cuentas de tus clientes. ## Errores comunes - Escribir sin `request_id`: sin él cuesta encontrar tu petición. - Enviar una captura con la llave a la vista. Si pasó, revócala. ## Qué sigue [Salud y estado](/docs/salud-y-estado/). --- # Herramientas OpenAPI, Postman, llms.txt, MCP público y clientes. Todo lo que necesitas para integrar se descarga sin cuenta y sale del mismo contrato. | Herramienta | Dónde | |---|---| | OpenAPI | [`/openapi.json`](https://merchants.fital.xyz/openapi.json), [`/openapi.yaml`](https://merchants.fital.xyz/openapi.yaml) | | OpenAPI de la cuenta de prueba | [`/docs/openapi.sandbox.json`](/docs/openapi.sandbox.json), con las dos rutas que sólo existen ahí | | Referencia por operación | [Una página por operación](/docs/operaciones/), también en Markdown | | Colección Postman | [`/postman/fital-b2b.postman_collection.json`](https://merchants.fital.xyz/postman/fital-b2b.postman_collection.json) | | Entorno Postman | [`/postman/fital-b2b-sandbox.postman_environment.json`](https://merchants.fital.xyz/postman/fital-b2b-sandbox.postman_environment.json) | | Índice para IA | [`/llms.txt`](https://merchants.fital.xyz/llms.txt), [`/llms-full.txt`](https://merchants.fital.xyz/llms-full.txt) | | Referencia en texto | [`/docs/referencia.md`](/docs/referencia.md) | | Script de webhooks | [`fital-webhook-test.sh`](/docs/herramientas/fital-webhook-test.sh) | | Hashes de este release | [`/docs/integridad.json`](/docs/integridad.json) | ## Postman La colección apunta sólo al simulador público y genera `Idempotency-Key` en cada petición. No hay entorno de producción: Postman sincroniza los entornos a su nube, así que no pongas ahí una llave real. ## Clientes Aún no hay bibliotecas oficiales. Las [muestras](/docs/primer-pago/) usan sólo la biblioteca estándar de cada lenguaje, y el OpenAPI sirve para generar un cliente. ## Servidor MCP Un servidor MCP de sólo lectura, en `https://widget.fital.xyz/explorar/mcp`, para que un asistente de IA consulte esta documentación y el simulador público. Tiene cuatro herramientas: `buscar_docs`, `leer_pagina`, `leer_operacion` y `probar_sandbox`. No acepta llaves ni mueve dinero. Su contenido son datos, no instrucciones. ## Errores comunes - Generar un cliente con una copia vieja del OpenAPI. Compara su hash con [`/docs/integridad.json`](/docs/integridad.json). - Pegar una llave real en Postman. ## Qué sigue [Cambios](/docs/cambios/). --- # Salud y estado Cómo saber si la API responde y qué capacidades están afectadas. [`GET /v1/health`](/docs/operaciones/consultar-salud/) dice si la API responde. [`GET /v1/status`](/docs/operaciones/consultar-estado/) dice qué capacidades de tu empresa están afectadas. No hay página pública de estado. | Ruta | Acceso | Para qué | Responde | |---|---|---|---| | [`GET /v1/health`](/docs/operaciones/consultar-salud/) | Pública | Saber si la API responde | `ok`, `degraded` o `down`, sin detalle | | [`GET /v1/status`](/docs/operaciones/consultar-estado/) | Con llave | Saber qué capacidades están afectadas | Estado por capacidad | `/v1/health` sale de una medición en segundo plano, sin llave y sin gastar cuota: | `status` | HTTP | Qué significa | |---|---|---| | `ok` | 200 | La API y sus servicios internos responden | | `degraded` | 200 | La API responde, pero un servicio interno no, como el que procesa los pagos; o la medición tiene más de 15 segundos | | `down` | 503 | La API no puede atender peticiones | `degraded` responde **200**: si tu monitor sólo mira el código HTTP, no lo verá. Lee también `status`. Capacidades: `payments_mxn`, `payments_latam`, `payments_pix`, `collections`, `webhooks`, `api`. Estados: `operational`, `degraded`, `outage`, `unknown`, `not_enabled`. Una capacidad que tu empresa no tiene sale como `not_enabled`. ## Qué hacer | Lo que ves | Qué hacer | |---|---| | `degraded` en `/v1/health` | Consulta [`GET /v1/status`](/docs/operaciones/consultar-estado/) para ver qué está afectado | | `degraded` en una capacidad | Sigue operando. Espera más demora y más reintentos | | `outage` en `payments_mxn` o `payments_latam` | Pausa pagos nuevos de esa capacidad. Conserva cola local. No generes claves nuevas | | `unknown` | Trátalo como `degraded` y revisa de nuevo en 1 minuto | | `outage` en `webhooks` | Consulta [`GET /v1/events`](/docs/operaciones/listar-eventos/) para recuperar eventos | | `503` | Reintenta con la misma clave y espera exponencial con variación | | Timeout en un pago | Repite con **la misma** `Idempotency-Key`. Nunca lo des por fallido | | `/v1/health` no responde | Caída total: ver abajo | Quien tiene login ve lo mismo en la pantalla Estado del panel, con incidentes e historial de 90 días. ## Caída total - `/v1/health` y `/v1/status` no responderán, y el panel probablemente tampoco. - Tu integración debe contemplarlo desde el inicio: cola local, reintentos con espera, tope de reintentos y alerta propia. - No cambies llaves, IP ni claves de idempotencia durante la caída. - Escribe a [soporte](/docs/soporte/), que no depende de la API. ## Errores comunes - Usar `/v1/health` para decidir si un pago ocurrió. Consulta el `pay_`. - Tratar sólo el `503` como problema: `degraded` llega con `200`. - Pausar todo por una capacidad degradada: pausa sólo lo afectado. ## Qué sigue [Soporte](/docs/soporte/). --- # Referencia de la API Texto completo del contrato, versión `2026-11-01`. Base de producción: `https://widget.fital.xyz/v1`. Hay una [página por operación](/docs/operaciones/), con muestras en 4 lenguajes, y una [referencia interactiva](/docs/referencia/). Los errores usan la forma de [Errores](/docs/errores.md), salvo las [dos excepciones](/docs/errores.md#forma-del-error) que esa página describe. Operaciones: 37. ## Cuenta ### GET /v1/balances Saldos disponibles Saldo disponible de la empresa dueña de la llave, una fila por moneda. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/balances` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `listBalances` | | Acceso | Llave con el scope `balances:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `data` | lista de objeto | Sí | Un saldo por moneda. | | `data[].available` | texto | Sí | Saldo disponible en esa moneda, como decimal en texto. | | `data[].currency` | texto | Sí | Moneda del saldo. | ```json noexec="ejemplo" { "data": [ { "available": "125000.00", "currency": "MXN" }, { "available": "4200.00", "currency": "USD" } ] } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) (sólo cuenta de prueba) | El encabezado `X-Fital-Scenario` trae un escenario desconocido. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "balances:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/listar-saldos/). #### Guías - [Conceptos](/docs/conceptos/) - [Conciliar](/docs/recetas/conciliar/) - [Autenticación](/docs/autenticacion/) - [Paginación](/docs/paginacion/) - [Direcciones de depósito](/docs/recetas/direcciones-deposito/) - [Pago masivo](/docs/recetas/pago-masivo/) ### GET /v1/limits Límites de la cuenta Monto máximo por operación, si existe uno fijo para la empresa, y rieles habilitados. Un riel fuera de `rails` no se puede usar. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/limits` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `getLimits` | | Acceso | Llave con el scope `balances:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | - En producción, `max_amount` llega en `null`: no hay tope por pago. Sí hay tope acumulado por empresa, y aplican los límites de cada riel. Ver [Límites](/docs/limites/#montos). En la cuenta de prueba trae el tope ficticio. #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `max_amount` | texto o `null` | Sí | Monto máximo por operación. Hoy siempre null: no hay tope por pago. Los pagos sí tienen tope acumulado por empresa, en la guía de límites. Ese tope y los del riel se aplican al ejecutar y responden `limit_exceeded`. | | `rails` | lista de texto | Sí | Rieles habilitados para la empresa. | ###### Valores de `rails` | Valor | Significado | |---|---| | `mxn_spei` | SPEI, en México. | | `usd_ach` | ACH, en Estados Unidos. | | `usd_wire` | Wire, en Estados Unidos. | | `eur_sepa` | SEPA, en la zona euro. | | `gbp_fps` | Faster Payments, en Reino Unido. | | `cop` | Bre-B, en Colombia. | | `pix` | PIX, en Brasil. | | `latam` | Transferencia local en otros países de América Latina. | `null` mientras el alta no se confirme. ```json noexec="ejemplo" { "max_amount": null, "rails": [ "mxn_spei" ] } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) (sólo cuenta de prueba) | El encabezado `X-Fital-Scenario` trae un escenario desconocido. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "balances:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/consultar-limites/). #### Guías - [Límites](/docs/limites/) - [Cobertura](/docs/cobertura/) - [Autenticación](/docs/autenticacion/) - [Conceptos](/docs/conceptos/) - [Pago ACH](/docs/recetas/pago-ach/) - [Pago Bre-B](/docs/recetas/pago-bre-b/) - [Pago FPS](/docs/recetas/pago-fps/) - [Pago LatAm](/docs/recetas/pago-latam/) - [Pago PIX](/docs/recetas/pago-pix/) - [Pago SEPA](/docs/recetas/pago-sepa/) - [Pago SPEI](/docs/recetas/pago-spei/) - [Pago wire](/docs/recetas/pago-wire/) ### GET /v1/receiving-accounts Cuentas para recibir Cuentas de la empresa para recibir fondos. Los campos que no apliquen a un tipo de cuenta llegan en null. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/receiving-accounts` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `listReceivingAccounts` | | Acceso | Llave con el scope `accounts:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `data` | lista de objeto | Sí | Cuentas de la empresa para recibir fondos. | | `data[].account_number` | texto o `null` | Sí | Número de cuenta. null si no aplica a esta cuenta. | | `data[].bank` | texto o `null` | Sí | Banco de la cuenta. null si no aplica a esta cuenta. | | `data[].bic` | texto o `null` | Sí | Código BIC. null si no aplica a esta cuenta. | | `data[].br_code` | texto o `null` | Sí | Código BR para pagar por PIX, en cuentas BRL. null si no aplica a esta cuenta. | | `data[].bre_b_key` | texto o `null` | Sí | Llave Bre-B, en cuentas COP. null si no aplica a esta cuenta. | | `data[].clabe` | texto o `null` | Sí | CLABE de 18 dígitos. null si no aplica a esta cuenta. | | `data[].currency` | texto o `null` | Sí | Moneda que recibe la cuenta. | | `data[].holder` | texto o `null` | Sí | Titular de la cuenta. null si no aplica a esta cuenta. | | `data[].iban` | texto o `null` | Sí | IBAN. null si no aplica a esta cuenta. | | `data[].pix_key` | texto o `null` | Sí | Llave PIX, en cuentas BRL. null si no aplica a esta cuenta. | | `data[].routing_number` | texto o `null` | Sí | Número de ruta bancaria. null si no aplica a esta cuenta. | | `data[].sort_code` | texto o `null` | Sí | Sort code, en cuentas GBP. null si no aplica a esta cuenta. | ```json noexec="ejemplo" { "data": [ { "account_number": null, "bank": "Banco de ejemplo", "bic": null, "br_code": null, "bre_b_key": null, "clabe": "999180000000000022", "currency": "MXN", "holder": "EMPRESA FICTICIA SA DE CV", "iban": null, "pix_key": null, "routing_number": null, "sort_code": null } ] } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) (sólo cuenta de prueba) | El encabezado `X-Fital-Scenario` trae un escenario desconocido. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "accounts:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/listar-cuentas-para-recibir/). #### Guías - [Conceptos](/docs/conceptos/) - [Cuentas para recibir](/docs/recetas/cuentas-para-recibir/) - [Autenticación](/docs/autenticacion/) - [Paginación](/docs/paginacion/) ### POST /v1/receiving-accounts Abrir una cuenta para recibir Abre una cuenta de la empresa en una moneda, si tu empresa tiene esa moneda activada. Hay una por moneda: si ya existe, responde 200 con esa misma cuenta y `already_existed: true`, nunca abre otra. Exige `Idempotency-Key` y firma. 201 si se abrió; 202 si aún no se confirma: repite con la misma clave. | Dato | Valor | |---|---| | Método y ruta | `POST /v1/receiving-accounts` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `createReceivingAccount` | | Acceso | Llave con el scope `accounts:write` | | Firma de petición | **Obligatoria** en producción y en la cuenta de prueba. El simulador público no la valida. Ver [Firma de petición](/docs/autenticacion/#firma-de-petición) | | Idempotencia | **Obligatoria**, con `Idempotency-Key`. Ver [Idempotencia](/docs/idempotencia/) | - Hay una cuenta por moneda. Si ya existe, responde `200` con esa misma cuenta y `already_existed: true`; nunca abre otra. - `202` significa que el alta aún no se confirma: los datos llegan en `null`. No compartas nada todavía. Repite con la misma clave. #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `Content-Type` | Sí | `application/json`. | | `Idempotency-Key` | Sí | Clave única de esta operación, de 8 a 200 caracteres. Repetirla con el mismo cuerpo devuelve la misma respuesta durante 7 días. Con otro cuerpo da `422 idempotency_key_reused`, también pasado ese plazo. | | `X-Fital-Timestamp` | Sí | Hora Unix en segundos. Se acepta hasta 300 s de diferencia. | | `X-Fital-Signature` | Sí | Firma `v1=` de la petición con el secreto de firma de la llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Cuerpo | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `currency` | texto | Sí | Moneda de la cuenta: `USD`, `EUR`, `BRL`, `GBP` o `COP`. No distingue mayúsculas. MXN no se abre aquí: tu CLABE ya aparece en `GET /v1/receiving-accounts`. | ```json noexec="ejemplo" { "currency": "USD" } ``` #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `account_number` | texto o `null` | Sí | Número de cuenta. null si no aplica a esta cuenta. | | `already_existed` | booleano | Sí | `true` si la empresa ya tenía una cuenta activa en esa moneda y esta es la misma: nunca se abre una segunda. | | `bank` | texto o `null` | Sí | Banco de la cuenta. null si no aplica a esta cuenta. | | `bic` | texto o `null` | Sí | Código BIC. null si no aplica a esta cuenta. | | `br_code` | texto o `null` | Sí | Código BR para pagar por PIX, en cuentas BRL. null si no aplica a esta cuenta. | | `bre_b_key` | texto o `null` | Sí | Llave Bre-B, en cuentas COP. null si no aplica a esta cuenta. | | `clabe` | texto o `null` | Sí | CLABE de 18 dígitos. null si no aplica a esta cuenta. | | `currency` | texto o `null` | Sí | Moneda que recibe la cuenta. | | `holder` | texto o `null` | Sí | Titular de la cuenta. null si no aplica a esta cuenta. | | `iban` | texto o `null` | Sí | IBAN. null si no aplica a esta cuenta. | | `pix_key` | texto o `null` | Sí | Llave PIX, en cuentas BRL. null si no aplica a esta cuenta. | | `routing_number` | texto o `null` | Sí | Número de ruta bancaria. null si no aplica a esta cuenta. | | `sort_code` | texto o `null` | Sí | Sort code, en cuentas GBP. null si no aplica a esta cuenta. | | `status` | texto | Sí | Estado de la cuenta. `processing`: El alta aún no se confirma: repite con la misma clave. Los datos llegan en null. `active`: Ya recibe fondos. | ###### Valores de `status` | Valor | Significado | |---|---| | `processing` | El alta aún no se confirma: repite con la misma clave. Los datos llegan en `null`. | | `active` | Ya recibe fondos. | ```json noexec="ejemplo" { "account_number": "900000000012", "already_existed": true, "bank": "Banco de ejemplo", "bic": null, "br_code": null, "bre_b_key": null, "clabe": null, "currency": "USD", "holder": "EMPRESA FICTICIA SA DE CV", "iban": null, "pix_key": null, "routing_number": "999999999", "sort_code": null, "status": "active" } ``` ##### 201 Creado Misma forma que la respuesta anterior. ```json noexec="ejemplo" { "account_number": "900000000012", "already_existed": false, "bank": "Banco de ejemplo", "bic": null, "br_code": null, "bre_b_key": null, "clabe": null, "currency": "USD", "holder": "EMPRESA FICTICIA SA DE CV", "iban": null, "pix_key": null, "routing_number": "999999999", "sort_code": null, "status": "active" } ``` ##### 202 Aceptado: el resultado final llega después Misma forma que la respuesta anterior. ```json noexec="ejemplo" { "account_number": null, "already_existed": false, "bank": null, "bic": null, "br_code": null, "bre_b_key": null, "clabe": null, "currency": "USD", "holder": null, "iban": null, "pix_key": null, "routing_number": null, "sort_code": null, "status": "processing" } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Idempotent-Replayed` | `true` si la respuesta repite una anterior con esa clave. | | `Retry-After` | Segundos a esperar, con `429`, con `409 idempotency_in_progress` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 400 | [`idempotency_key_required`](/docs/errores/#idempotency_key_required) | Falta `Idempotency-Key`. | `false` | | 400 | [`idempotency_key_invalid`](/docs/errores/#idempotency_key_invalid) | La clave no mide entre 8 y 200 caracteres, o viene duplicada. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 401 | [`invalid_signature`](/docs/errores/#invalid_signature) | Falta la firma, no coincide o tiene más de 5 minutos. | `false` | | 403 | [`account_restricted`](/docs/errores/#account_restricted) | La cuenta tiene una restricción. | `false` | | 403 | [`not_enabled`](/docs/errores/#not_enabled) | Esa moneda no está activada para tu empresa. No se abrió nada. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 409 | [`receiving_account_in_progress`](/docs/errores/#receiving_account_in_progress) | Otra alta de esa moneda sigue en curso. Reintenta en unos segundos. | `true` | | 409 | [`idempotency_in_progress`](/docs/errores/#idempotency_in_progress) | Una petición anterior con esa clave sigue en curso. Llega con `Retry-After`. | `true` | | 413 | [`invalid_request`](/docs/errores/#invalid_request) | El cuerpo supera 64 KB. | `false` | | 422 | [`in_review`](/docs/errores/#in_review) | Una revisión de tu cuenta frena la operación. | `true` | | 422 | [`currency_not_supported`](/docs/errores/#currency_not_supported) | Esa moneda no se abre por API. `MXN` responde esto: tu CLABE ya aparece en [`GET /v1/receiving-accounts`](/docs/operaciones/listar-cuentas-para-recibir/). | `false` | | 422 | [`receiving_account_rejected`](/docs/errores/#receiving_account_rejected) | La cuenta no se pudo abrir. No se reintenta sola. | `false` | | 422 | [`idempotency_key_reused`](/docs/errores/#idempotency_key_reused) | Usaste esa clave con otro cuerpo, también pasados 7 días. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "currency": "Valor inválido" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 account_restricted`: ```json noexec="ejemplo" { "error": { "code": "account_restricted", "message": "Tu cuenta tiene una restricción. Contacta a tu ejecutivo FITAL.", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#account_restricted" } } ``` `409 receiving_account_in_progress`: ```json noexec="ejemplo" { "error": { "code": "receiving_account_in_progress", "message": "Operación rechazada", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#receiving_account_in_progress" } } ``` `413 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `422 in_review`: ```json noexec="ejemplo" { "error": { "code": "in_review", "message": "La operación está en revisión", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#in_review" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Ante un timeout o una conexión cortada, repite con la misma clave. - `409 receiving_account_in_progress`: Sí, con espera - `409 idempotency_in_progress`: Espera `Retry-After` y repite con la misma clave. - `422 in_review`: Mira `retryable`. Con `true`, repite con la misma clave cuando la revisión se levante. Con `false`, el rechazo quedó registrado y esa clave lo repite: usa una clave nueva tras la revisión. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/abrir-cuenta-para-recibir/). #### Guías - [Cuentas para recibir](/docs/recetas/cuentas-para-recibir/) - [Cobertura](/docs/cobertura/) - [Autenticación](/docs/autenticacion/) - [Conceptos](/docs/conceptos/) - [Cuenta de prueba](/docs/sandbox/cuenta-de-prueba/) ### GET /v1/wallets Wallets de la empresa Las wallets de la empresa dueña de la llave, con su red y su saldo disponible. Sólo consulta: las wallets no se crean ni se modifican por API. Una red puede tener más de una wallet. La suma por moneda es lo que da `GET /v1/balances`, salvo redondeo al centavo. Lo apartado por compras con tarjeta se descuenta primero de la wallet con más saldo. Para recibir fondos usa `GET /v1/deposit-addresses`. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/wallets` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `listWallets` | | Acceso | Llave con el scope `wallets:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | - Sólo consulta: las wallets no se crean ni se modifican por API. Para recibir fondos usa [`GET /v1/deposit-addresses`](/docs/operaciones/listar-direcciones-de-deposito/). - La suma por moneda es lo que da [`GET /v1/balances`](/docs/operaciones/listar-saldos/), salvo redondeo al centavo. Lo apartado por compras con tarjeta se descuenta una vez, primero de la wallet con más saldo. #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `data` | lista de objeto | Sí | Todas las wallets de la empresa, ordenadas por red. | | `data[].balances` | lista de objeto | Sí | Saldo disponible de esta wallet, una fila por moneda, en las mismas monedas de `GET /v1/balances`: USD y EUR. Lista vacía si no tiene saldo en ellas. | | `data[].balances[].available` | texto | Sí | Saldo disponible en esa moneda, como decimal en texto. | | `data[].balances[].currency` | texto | Sí | Moneda del saldo. | | `data[].chain` | texto o `null` | Sí | Red de la wallet, en minúsculas, por ejemplo `base`. | | `data[].id` | texto | Sí | Identificador de la wallet. No cambia y sólo vale para tu empresa. | ```json noexec="ejemplo" { "data": [ { "balances": [ { "available": "4200.00", "currency": "USD" } ], "chain": "base", "id": "wal_5f2c8e1ab37d4c60918e2d7f4a6b0c13" }, { "balances": [ { "available": "0.00", "currency": "USD" } ], "chain": "solana", "id": "wal_a94d03be6c1f47d2b8e05c7e12fd6a38" } ] } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) (sólo cuenta de prueba) | El encabezado `X-Fital-Scenario` trae un escenario desconocido. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "wallets:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/listar-wallets/). #### Guías - [Conceptos](/docs/conceptos/) - [Conciliar](/docs/recetas/conciliar/) - [Autenticación](/docs/autenticacion/) - [Paginación](/docs/paginacion/) ## Beneficiarios ### GET /v1/beneficiaries Beneficiarios Beneficiarios de la empresa, con su estado. Paginación por cursor. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/beneficiaries` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `listBeneficiaries` | | Acceso | Llave con el scope `beneficiaries:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Parámetros | Parámetro | En | Tipo | Obligatorio | Descripción | |---|---|---|---|---| | `limit` | consulta | texto | No | Filas por página, de 1 a 100. Por omisión, 25. | | `cursor` | consulta | texto | No | Valor de `next_cursor` de la página anterior. Hasta 1000 caracteres. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `data` | lista de objeto | Sí | Filas de esta página. | | `data[].country` | texto o `null` | Sí | País de la cuenta, si se conoce. | | `data[].created_at` | texto o `null` | Sí | Fecha de creación. ISO 8601 en UTC. | | `data[].id` | texto | Sí | Identificador del beneficiario, con prefijo `ben_`. | | `data[].rail` | texto o `null` | Sí | Riel del beneficiario. null mientras el alta no se confirme. `mxn_spei`: SPEI, en México. `usd_ach`: ACH, en Estados Unidos. `usd_wire`: Wire, en Estados Unidos. `eur_sepa`: SEPA, en la zona euro. `gbp_fps`: Faster Payments, en Reino Unido. `cop`: Bre-B, en Colombia. `pix`: PIX, en Brasil. `latam`: Transferencia local en otros países de América Latina. | | `data[].status` | texto | Sí | Sólo con `approved` se le puede pagar. La aprobación se hace en el panel. `processing`: El alta sigue en curso. `pending_approval`: Falta aprobarlo en el panel. `approved`: Ya se le puede pagar. `rejected`: No se aprobó. `deleted`: Se dio de baja. | | `has_more` | booleano | Sí | true si hay más filas después de esta página. | | `next_cursor` | texto o `null` | Sí | Cursor de la página siguiente. null si no hay. | ###### Valores de `data[].rail` | Valor | Significado | |---|---| | `mxn_spei` | SPEI, en México. | | `usd_ach` | ACH, en Estados Unidos. | | `usd_wire` | Wire, en Estados Unidos. | | `eur_sepa` | SEPA, en la zona euro. | | `gbp_fps` | Faster Payments, en Reino Unido. | | `cop` | Bre-B, en Colombia. | | `pix` | PIX, en Brasil. | | `latam` | Transferencia local en otros países de América Latina. | `null` mientras el alta no se confirme. ###### Valores de `data[].status` | Valor | Significado | |---|---| | `processing` | El alta sigue en curso. | | `pending_approval` | Falta aprobarlo en el panel. | | `approved` | Ya se le puede pagar. | | `rejected` | No se aprobó. | | `deleted` | Se dio de baja. | ```json noexec="ejemplo" { "data": [ { "country": "MX", "created_at": "2026-11-04T18:00:00.000Z", "id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "rail": "mxn_spei", "status": "approved" } ], "has_more": false, "next_cursor": null } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "beneficiaries:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/listar-beneficiarios/). #### Guías - [Pago SPEI](/docs/recetas/pago-spei/) - [Paginación](/docs/paginacion/) - [Autenticación](/docs/autenticacion/) ### POST /v1/beneficiaries Alta de beneficiario, pendiente de aprobar en el panel Da de alta una cuenta destino. Nace pendiente y se aprueba en el panel, nunca por API: una llave robada no paga a un destino nuevo. Exige `Idempotency-Key` y firma. 201 si el alta quedó registrada; 202 si aún no se confirma: repite con la misma clave. | Dato | Valor | |---|---| | Método y ruta | `POST /v1/beneficiaries` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `createBeneficiary` | | Acceso | Llave con el scope `beneficiaries:write` | | Firma de petición | **Obligatoria** en producción y en la cuenta de prueba. El simulador público no la valida. Ver [Firma de petición](/docs/autenticacion/#firma-de-petición) | | Idempotencia | **Obligatoria**, con `Idempotency-Key`. Ver [Idempotencia](/docs/idempotencia/) | - `rail` decide qué campos se piden: cada variante del cuerpo tiene los suyos. - En `latam`, `entity`, `account_type`, `document_type` y `document` dependen del país: [`GET /v1/latam/countries`](/docs/operaciones/listar-paises-latam/) dice qué pide cada uno. En `pix`, `document` es opcional. - Si la misma cuenta ya estaba registrada, la respuesta trae el beneficiario que ya existe, con su `id` y su estado. Guarda el `id` que recibes, no el que esperabas. - `202` significa que el alta aún no se confirma: el `id` ya es válido y `rail`, `country` y `created_at` llegan en `null`. Repite con la misma clave, o consulta el `id`. - El beneficiario nace `pending_approval` y se aprueba en el panel, nunca por API. En la cuenta de prueba nace `approved`. #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `Content-Type` | Sí | `application/json`. | | `Idempotency-Key` | Sí | Clave única de esta operación, de 8 a 200 caracteres. Repetirla con el mismo cuerpo devuelve la misma respuesta durante 7 días. Con otro cuerpo da `422 idempotency_key_reused`, también pasado ese plazo. | | `X-Fital-Timestamp` | Sí | Hora Unix en segundos. Se acepta hasta 300 s de diferencia. | | `X-Fital-Signature` | Sí | Firma `v1=` de la petición con el secreto de firma de la llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Cuerpo ##### Con `rail: mxn_spei` | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `clabe` | texto | Sí | CLABE de 18 dígitos. | | `holder_name` | texto | Sí | Titular de la cuenta destino. Hasta 140 caracteres. | | `rail` | texto | Sí | Riel de pago. Decide qué campos se piden. `mxn_spei`: SPEI, en México. | ```json noexec="ejemplo" { "rail": "mxn_spei", "holder_name": "PROVEEDOR FICTICIO SA DE CV", "clabe": "999180000000000011" } ``` ##### Con `rail: usd_ach` | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `account_number` | texto | Sí | Número de cuenta, de 4 a 17 dígitos. | | `account_type` | texto | Sí | Tipo de cuenta. `checking`: Cuenta de cheques o corriente. `savings`: Cuenta de ahorro. | | `address` | objeto | Sí | Domicilio del titular. | | `address.city` | texto | Sí | Ciudad. Hasta 100 caracteres. | | `address.country` | texto | Sí | País, en código de 3 letras. 3 caracteres. | | `address.postal_code` | texto | Sí | Código postal. Hasta 20 caracteres. | | `address.state` | texto | Sí | Estado o provincia. Hasta 100 caracteres. | | `address.street_line_1` | texto | Sí | Calle y número. De 4 a 35 caracteres. | | `holder_name` | texto | Sí | Titular de la cuenta destino. Hasta 140 caracteres. | | `rail` | texto | Sí | Riel de pago. Decide qué campos se piden. `usd_ach`: ACH, en Estados Unidos. | | `routing_number` | texto | Sí | Número de ruta, 9 dígitos. | ```json noexec="ejemplo" { "rail": "usd_ach", "holder_name": "PROVEEDOR FICTICIO LLC", "routing_number": "999999999", "account_number": "000123456789", "account_type": "checking", "address": { "street_line_1": "100 Calle Ficticia", "city": "Ciudad Ficticia", "state": "TX", "postal_code": "00000", "country": "USA" } } ``` ##### Con `rail: usd_wire` | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `account_number` | texto | Sí | Número de cuenta, de 4 a 17 dígitos. | | `account_type` | texto | Sí | Tipo de cuenta. `checking`: Cuenta de cheques o corriente. `savings`: Cuenta de ahorro. | | `address` | objeto | Sí | Domicilio del titular. | | `address.city` | texto | Sí | Ciudad. Hasta 100 caracteres. | | `address.country` | texto | Sí | País, en código de 3 letras. 3 caracteres. | | `address.postal_code` | texto | Sí | Código postal. Hasta 20 caracteres. | | `address.state` | texto | Sí | Estado o provincia. Hasta 100 caracteres. | | `address.street_line_1` | texto | Sí | Calle y número. De 4 a 35 caracteres. | | `holder_name` | texto | Sí | Titular de la cuenta destino. Hasta 140 caracteres. | | `rail` | texto | Sí | Riel de pago. Decide qué campos se piden. `usd_wire`: Wire, en Estados Unidos. | | `routing_number` | texto | Sí | Número de ruta, 9 dígitos. | ```json noexec="ejemplo" { "rail": "usd_wire", "holder_name": "PROVEEDOR FICTICIO LLC", "routing_number": "999999999", "account_number": "000123456789", "account_type": "checking", "address": { "street_line_1": "100 Calle Ficticia", "city": "Ciudad Ficticia", "state": "TX", "postal_code": "00000", "country": "USA" } } ``` ##### Con `rail: eur_sepa` | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `bic` | texto | Sí | Código BIC del banco. De 8 a 11 caracteres. | | `holder_name` | texto | Sí | Titular de la cuenta destino. Hasta 140 caracteres. | | `iban` | texto | Sí | IBAN de la cuenta. De 15 a 34 caracteres. | | `iban_country` | texto | Sí | País del IBAN, en código de 3 letras. 3 caracteres. | | `rail` | texto | Sí | Riel de pago. Decide qué campos se piden. `eur_sepa`: SEPA, en la zona euro. | ```json noexec="ejemplo" { "rail": "eur_sepa", "holder_name": "PROVEEDOR FICTICIO SL", "iban": "XX00FICTICIO0000000000", "bic": "FICTXXXX", "iban_country": "ESP" } ``` ##### Con `rail: gbp_fps` | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `account_number` | texto | Sí | Número de cuenta, 8 dígitos. | | `holder_name` | texto | Sí | Titular de la cuenta destino. Hasta 140 caracteres. | | `rail` | texto | Sí | Riel de pago. Decide qué campos se piden. `gbp_fps`: Faster Payments, en Reino Unido. | | `sort_code` | texto | Sí | Sort code, 6 dígitos. | ```json noexec="ejemplo" { "rail": "gbp_fps", "holder_name": "PROVEEDOR FICTICIO LTD", "account_number": "00000000", "sort_code": "000000" } ``` ##### Con `rail: cop` | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `bre_b_key` | texto | Sí | Llave Bre-B del titular. Hasta 100 caracteres. | | `holder_name` | texto | Sí | Titular de la cuenta destino. Hasta 140 caracteres. | | `rail` | texto | Sí | Riel de pago. Decide qué campos se piden. `cop`: Bre-B, en Colombia. | ```json noexec="ejemplo" { "rail": "cop", "holder_name": "PROVEEDOR FICTICIO SAS", "bre_b_key": "llave-bre-b-de-ejemplo" } ``` ##### Con `rail: pix` | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `document` | texto | No | Documento del titular. Hasta 20 caracteres. | | `holder_name` | texto | Sí | Titular de la cuenta destino. Hasta 140 caracteres. | | `pix_key` | texto | Sí | Llave PIX del titular. Hasta 100 caracteres. | | `rail` | texto | Sí | Riel de pago. Decide qué campos se piden. `pix`: PIX, en Brasil. | ```json noexec="ejemplo" { "rail": "pix", "holder_name": "PROVEEDOR FICTICIO LTDA", "pix_key": "llave-pix-de-ejemplo", "document": "00000000000" } ``` ##### Con `rail: latam` | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `account_number` | texto | Sí | Número de cuenta. `GET /v1/latam/countries` dice qué pide cada país. De 4 a 40 caracteres. | | `account_type` | texto | No | Tipo de cuenta, si el país lo pide. `GET /v1/latam/countries` dice qué pide cada país. `checking`: Cuenta de cheques o corriente. `savings`: Cuenta de ahorro. | | `country` | texto | Sí | País de la cuenta, en código de 2 letras. 2 caracteres. | | `currency` | texto | Sí | Moneda de la cuenta, en código de 3 letras. 3 caracteres. | | `document` | texto | No | Documento del titular, si el país lo pide. Hasta 30 caracteres. | | `document_type` | texto | No | Tipo de documento del titular, si el país lo pide. `GET /v1/latam/countries` dice qué pide cada país. Hasta 20 caracteres. | | `entity` | texto | No | Banco o entidad, si el país lo pide. Los valores salen de `GET /v1/latam/countries/{country}/entities`. Hasta 40 caracteres. | | `holder_name` | texto | Sí | Titular de la cuenta destino. Hasta 140 caracteres. | | `rail` | texto | Sí | Riel de pago. Decide qué campos se piden. `latam`: Transferencia local en otros países de América Latina. | ```json noexec="ejemplo" { "rail": "latam", "holder_name": "PROVEEDOR FICTICIO SPA", "country": "CL", "currency": "CLP", "account_number": "00000000", "entity": "000", "document_type": "RUT", "document": "00000000-0" } ``` ##### Valores de `rail` | Valor | Significado | |---|---| | `mxn_spei` | SPEI, en México. Ver [Pago SPEI](/docs/recetas/pago-spei/). | | `usd_ach` | ACH, en Estados Unidos. Ver [Pago ACH](/docs/recetas/pago-ach/). | | `usd_wire` | Wire, en Estados Unidos. Ver [Pago wire](/docs/recetas/pago-wire/). | | `eur_sepa` | SEPA, en la zona euro. Ver [Pago SEPA](/docs/recetas/pago-sepa/). | | `gbp_fps` | Faster Payments, en Reino Unido. Ver [Pago FPS](/docs/recetas/pago-fps/). | | `cop` | Bre-B, en Colombia. Ver [Pago Bre-B](/docs/recetas/pago-bre-b/). | | `pix` | PIX, en Brasil. Ver [Pago PIX](/docs/recetas/pago-pix/). | | `latam` | Transferencia local en otros países de América Latina. Ver [Pago LatAm](/docs/recetas/pago-latam/). | ##### Valores de `account_type` | Valor | Significado | |---|---| | `checking` | Cuenta de cheques o corriente. | | `savings` | Cuenta de ahorro. | #### Respuestas correctas ##### 201 Creado | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `country` | texto o `null` | Sí | País de la cuenta, si se conoce. | | `created_at` | texto o `null` | Sí | Fecha de creación. ISO 8601 en UTC. | | `id` | texto | Sí | Identificador del beneficiario, con prefijo `ben_`. | | `rail` | texto o `null` | Sí | Riel del beneficiario. null mientras el alta no se confirme. `mxn_spei`: SPEI, en México. `usd_ach`: ACH, en Estados Unidos. `usd_wire`: Wire, en Estados Unidos. `eur_sepa`: SEPA, en la zona euro. `gbp_fps`: Faster Payments, en Reino Unido. `cop`: Bre-B, en Colombia. `pix`: PIX, en Brasil. `latam`: Transferencia local en otros países de América Latina. | | `status` | texto | Sí | Sólo con `approved` se le puede pagar. La aprobación se hace en el panel. `processing`: El alta sigue en curso. `pending_approval`: Falta aprobarlo en el panel. `approved`: Ya se le puede pagar. `rejected`: No se aprobó. `deleted`: Se dio de baja. | ###### Valores de `rail` | Valor | Significado | |---|---| | `mxn_spei` | SPEI, en México. | | `usd_ach` | ACH, en Estados Unidos. | | `usd_wire` | Wire, en Estados Unidos. | | `eur_sepa` | SEPA, en la zona euro. | | `gbp_fps` | Faster Payments, en Reino Unido. | | `cop` | Bre-B, en Colombia. | | `pix` | PIX, en Brasil. | | `latam` | Transferencia local en otros países de América Latina. | `null` mientras el alta no se confirme. ###### Valores de `status` | Valor | Significado | |---|---| | `processing` | El alta sigue en curso. | | `pending_approval` | Falta aprobarlo en el panel. | | `approved` | Ya se le puede pagar. | | `rejected` | No se aprobó. | | `deleted` | Se dio de baja. | ```json noexec="ejemplo" { "country": "MX", "created_at": "2026-11-04T18:00:00.000Z", "id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "rail": "mxn_spei", "status": "pending_approval" } ``` ##### 202 Aceptado: el resultado final llega después Misma forma que la respuesta anterior. ```json noexec="ejemplo" { "country": null, "created_at": null, "id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "rail": null, "status": "processing" } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Idempotent-Replayed` | `true` si la respuesta repite una anterior con esa clave. | | `Location` | Ruta del recurso, con `201` y `202`. | | `Retry-After` | Segundos a esperar, con `429`, con `409 idempotency_in_progress` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún campo no cumple el esquema de su riel. En `usd_ach` y `usd_wire`, también si `address.street_line_1` no trae número de calle. | `false` | | 400 | [`idempotency_key_required`](/docs/errores/#idempotency_key_required) | Falta `Idempotency-Key`. | `false` | | 400 | [`idempotency_key_invalid`](/docs/errores/#idempotency_key_invalid) | La clave no mide entre 8 y 200 caracteres, o viene duplicada. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 401 | [`invalid_signature`](/docs/errores/#invalid_signature) | Falta la firma, no coincide o tiene más de 5 minutos. | `false` | | 403 | [`account_restricted`](/docs/errores/#account_restricted) | La cuenta tiene una restricción. | `false` | | 403 | [`rail_not_enabled`](/docs/errores/#rail_not_enabled) | El riel no está habilitado para tu empresa. En la cuenta de prueba, cualquier riel que no sea `mxn_spei`. | `false` | | 403 | [`not_enabled`](/docs/errores/#not_enabled) | El riel `latam` no está activado para tu empresa. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 409 | [`beneficiary_ref_conflict`](/docs/errores/#beneficiary_ref_conflict) | El identificador del alta ya existe. | `false` | | 409 | [`idempotency_in_progress`](/docs/errores/#idempotency_in_progress) | Una petición anterior con esa clave sigue en curso. Llega con `Retry-After`. | `true` | | 413 | [`invalid_request`](/docs/errores/#invalid_request) | El cuerpo supera 64 KB. | `false` | | 422 | [`in_review`](/docs/errores/#in_review) | Una revisión de tu cuenta frena las altas nuevas. | `true` | | 422 | [`invalid_beneficiary`](/docs/errores/#invalid_beneficiary) | El riel rechazó la cuenta destino por sus datos. En `latam`, también un número, banco, documento, moneda o tipo de cuenta que no cumple lo que pide el país: se responde al momento. | `false` | | 422 | [`idempotency_key_reused`](/docs/errores/#idempotency_key_reused) | Usaste esa clave con otro cuerpo, también pasados 7 días. | `false` | | 422 | [`sandbox_real_account_rejected`](/docs/errores/#sandbox_real_account_rejected) (sólo cuenta de prueba) | La CLABE no empieza con `999` o el titular no incluye `FICTICIO`. | `false` | | 422 | [`limit_exceeded`](/docs/errores/#limit_exceeded) (sólo cuenta de prueba) | Ya tienes 20 beneficiarios. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "clabe": "Valor inválido" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 account_restricted`: ```json noexec="ejemplo" { "error": { "code": "account_restricted", "message": "Tu cuenta tiene una restricción. Contacta a tu ejecutivo FITAL.", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#account_restricted" } } ``` `409 beneficiary_ref_conflict`: ```json noexec="ejemplo" { "error": { "code": "beneficiary_ref_conflict", "message": "Operación rechazada", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#beneficiary_ref_conflict" } } ``` `413 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `422 in_review`: ```json noexec="ejemplo" { "error": { "code": "in_review", "message": "La operación está en revisión", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#in_review" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Ante un timeout o una conexión cortada, repite con la misma clave. - `409 idempotency_in_progress`: Espera `Retry-After` y repite con la misma clave. - `422 in_review`: Mira `retryable`. Con `true`, repite con la misma clave cuando la revisión se levante. Con `false`, el rechazo quedó registrado y esa clave lo repite: usa una clave nueva tras la revisión. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/crear-beneficiario/). #### Guías - [Pago SPEI](/docs/recetas/pago-spei/) - [Pago ACH](/docs/recetas/pago-ach/) - [Pago wire](/docs/recetas/pago-wire/) - [Pago SEPA](/docs/recetas/pago-sepa/) - [Pago FPS](/docs/recetas/pago-fps/) - [Pago Bre-B](/docs/recetas/pago-bre-b/) - [Pago PIX](/docs/recetas/pago-pix/) - [Pago LatAm](/docs/recetas/pago-latam/) - [Cobertura](/docs/cobertura/) - [Autenticación](/docs/autenticacion/) - [Lista de migración](/docs/migracion/) ### GET /v1/beneficiaries/{id} Un beneficiario Un beneficiario de tu empresa. Si el alta quedó sin confirmar responde `processing`. El id de otra empresa da 404. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/beneficiaries/{id}` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `getBeneficiary` | | Acceso | Llave con el scope `beneficiaries:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Parámetros | Parámetro | En | Tipo | Obligatorio | Descripción | |---|---|---|---|---| | `id` | ruta | texto | Sí | Identificador del recurso. Hasta 250 caracteres. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `country` | texto o `null` | Sí | País de la cuenta, si se conoce. | | `created_at` | texto o `null` | Sí | Fecha de creación. ISO 8601 en UTC. | | `id` | texto | Sí | Identificador del beneficiario, con prefijo `ben_`. | | `rail` | texto o `null` | Sí | Riel del beneficiario. null mientras el alta no se confirme. `mxn_spei`: SPEI, en México. `usd_ach`: ACH, en Estados Unidos. `usd_wire`: Wire, en Estados Unidos. `eur_sepa`: SEPA, en la zona euro. `gbp_fps`: Faster Payments, en Reino Unido. `cop`: Bre-B, en Colombia. `pix`: PIX, en Brasil. `latam`: Transferencia local en otros países de América Latina. | | `status` | texto | Sí | Sólo con `approved` se le puede pagar. La aprobación se hace en el panel. `processing`: El alta sigue en curso. `pending_approval`: Falta aprobarlo en el panel. `approved`: Ya se le puede pagar. `rejected`: No se aprobó. `deleted`: Se dio de baja. | ###### Valores de `rail` | Valor | Significado | |---|---| | `mxn_spei` | SPEI, en México. | | `usd_ach` | ACH, en Estados Unidos. | | `usd_wire` | Wire, en Estados Unidos. | | `eur_sepa` | SEPA, en la zona euro. | | `gbp_fps` | Faster Payments, en Reino Unido. | | `cop` | Bre-B, en Colombia. | | `pix` | PIX, en Brasil. | | `latam` | Transferencia local en otros países de América Latina. | `null` mientras el alta no se confirme. ###### Valores de `status` | Valor | Significado | |---|---| | `processing` | El alta sigue en curso. | | `pending_approval` | Falta aprobarlo en el panel. | | `approved` | Ya se le puede pagar. | | `rejected` | No se aprobó. | | `deleted` | Se dio de baja. | ```json noexec="ejemplo" { "country": "MX", "created_at": "2026-11-04T18:00:00.000Z", "id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "rail": "mxn_spei", "status": "approved" } ``` Falta aprobarlo en el panel: ```json noexec="ejemplo" { "country": "MX", "created_at": "2026-11-04T18:00:00.000Z", "id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "rail": "mxn_spei", "status": "pending_approval" } ``` Alta sin confirmar: ```json noexec="ejemplo" { "country": null, "created_at": null, "id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "rail": null, "status": "processing" } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 404 | [`not_found`](/docs/errores/#not_found) | El beneficiario no existe o es de otra empresa. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "beneficiaries:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `404 not_found`: ```json noexec="ejemplo" { "error": { "code": "not_found", "message": "No encontrado", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#not_found" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/consultar-beneficiario/). #### Guías - [Pago SPEI](/docs/recetas/pago-spei/) - [Conceptos](/docs/conceptos/) - [Autenticación](/docs/autenticacion/) ### DELETE /v1/beneficiaries/{id} Baja de beneficiario Da de baja un beneficiario. Exige `Idempotency-Key` y firma. 202 si la baja aún no se confirma: repite con la misma clave. | Dato | Valor | |---|---| | Método y ruta | `DELETE /v1/beneficiaries/{id}` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `deleteBeneficiary` | | Acceso | Llave con el scope `beneficiaries:write` | | Firma de petición | **Obligatoria** en producción y en la cuenta de prueba. El simulador público no la valida. Ver [Firma de petición](/docs/autenticacion/#firma-de-petición) | | Idempotencia | **Obligatoria**, con `Idempotency-Key`. Ver [Idempotencia](/docs/idempotencia/) | - Un pago ya creado conserva su riel: dar de baja al beneficiario no lo cambia. #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `Idempotency-Key` | Sí | Clave única de esta operación, de 8 a 200 caracteres. Repetirla con el mismo cuerpo devuelve la misma respuesta durante 7 días. Con otro cuerpo da `422 idempotency_key_reused`, también pasado ese plazo. | | `X-Fital-Timestamp` | Sí | Hora Unix en segundos. Se acepta hasta 300 s de diferencia. | | `X-Fital-Signature` | Sí | Firma `v1=` de la petición con el secreto de firma de la llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Parámetros | Parámetro | En | Tipo | Obligatorio | Descripción | |---|---|---|---|---| | `id` | ruta | texto | Sí | Identificador del recurso. Hasta 250 caracteres. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `id` | texto | Sí | Identificador del beneficiario, con prefijo `ben_`. | | `status` | texto | Sí | Resultado de la baja. `deleted`: La baja quedó confirmada. `processing`: La baja aún no se confirma: repite con la misma clave. | ###### Valores de `status` | Valor | Significado | |---|---| | `deleted` | La baja quedó confirmada. | | `processing` | La baja aún no se confirma: repite con la misma clave. | ```json noexec="ejemplo" { "id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "status": "deleted" } ``` ##### 202 Aceptado: el resultado final llega después Misma forma que la respuesta anterior. ```json noexec="ejemplo" { "id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "status": "deleted" } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Idempotent-Replayed` | `true` si la respuesta repite una anterior con esa clave. | | `Retry-After` | Segundos a esperar, con `429`, con `409 idempotency_in_progress` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 400 | [`idempotency_key_required`](/docs/errores/#idempotency_key_required) | Falta `Idempotency-Key`. | `false` | | 400 | [`idempotency_key_invalid`](/docs/errores/#idempotency_key_invalid) | La clave no mide entre 8 y 200 caracteres, o viene duplicada. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 401 | [`invalid_signature`](/docs/errores/#invalid_signature) | Falta la firma, no coincide o tiene más de 5 minutos. | `false` | | 403 | [`account_restricted`](/docs/errores/#account_restricted) | La cuenta tiene una restricción. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 404 | [`not_found`](/docs/errores/#not_found) | El beneficiario no existe o es de otra empresa. | `false` | | 409 | [`beneficiary_state_conflict`](/docs/errores/#beneficiary_state_conflict) | Sólo se da de baja un beneficiario `pending_approval` o `approved`. | `false` | | 409 | [`idempotency_in_progress`](/docs/errores/#idempotency_in_progress) | Una petición anterior con esa clave sigue en curso. Llega con `Retry-After`. | `true` | | 422 | [`in_review`](/docs/errores/#in_review) | Una revisión de tu cuenta frena esta operación. | `true` | | 422 | [`idempotency_key_reused`](/docs/errores/#idempotency_key_reused) | Usaste esa clave con otro cuerpo, también pasados 7 días. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 account_restricted`: ```json noexec="ejemplo" { "error": { "code": "account_restricted", "message": "Tu cuenta tiene una restricción. Contacta a tu ejecutivo FITAL.", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#account_restricted" } } ``` `404 not_found`: ```json noexec="ejemplo" { "error": { "code": "not_found", "message": "No encontrado", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#not_found" } } ``` `409 beneficiary_state_conflict`: ```json noexec="ejemplo" { "error": { "code": "beneficiary_state_conflict", "message": "Operación rechazada", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#beneficiary_state_conflict" } } ``` `422 in_review`: ```json noexec="ejemplo" { "error": { "code": "in_review", "message": "La operación está en revisión", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#in_review" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Ante un timeout o una conexión cortada, repite con la misma clave. - `409 idempotency_in_progress`: Espera `Retry-After` y repite con la misma clave. - `422 in_review`: Mira `retryable`. Con `true`, repite con la misma clave cuando la revisión se levante. Con `false`, el rechazo quedó registrado y esa clave lo repite: usa una clave nueva tras la revisión. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/eliminar-beneficiario/). #### Guías - [Conceptos](/docs/conceptos/) - [Autenticación](/docs/autenticacion/) ### GET /v1/latam/countries Países del riel `latam` Países a los que tu empresa puede pagar por el riel `latam`, con su moneda y lo que cada uno pide para dar de alta una cuenta. Sale de la configuración vigente: consúltalo en vez de copiarlo. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/latam/countries` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `listLatamCountries` | | Acceso | Llave con el scope `beneficiaries:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | - Sale de la configuración vigente y sólo trae los países que tu empresa puede pagar: consúltalo antes de un alta, en vez de copiar la tabla de [Cobertura](/docs/cobertura/#países-del-riel-latam). - `available: false` significa que el país está restringido por ahora: el alta y los pagos pueden fallar. - `account` dice qué pide el alta: `entity_required`, `account_type_required` con sus valores en `account_types`, y el largo del número. Un largo que no aplica llega en `null`. - En la cuenta de prueba los países son ficticios y todos llegan con `available: false`: ese ambiente sólo simula SPEI, así que el alta `latam` no procede. #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `data` | lista de objeto | Sí | Países del riel `latam` para tu empresa. | | `data[].account` | objeto | Sí | Qué se pide de la cuenta. | | `data[].account.account_type_required` | booleano | Sí | true si el alta exige `account_type`. | | `data[].account.account_types` | lista de texto | Sí | Valores admitidos en `account_type`. | | `data[].account.digits_only` | booleano | Sí | true si sólo admite dígitos. | | `data[].account.entity_required` | booleano | Sí | true si el alta exige `entity`. | | `data[].account.exact_length` | entero o `null` | Sí | Largo exacto. null si no aplica. | | `data[].account.label` | texto o `null` | Sí | Cómo se llama el número de cuenta en ese país. | | `data[].account.max_length` | entero o `null` | Sí | Largo máximo. null si no aplica. | | `data[].account.min_length` | entero o `null` | Sí | Largo mínimo. null si no aplica. | | `data[].available` | booleano | Sí | false mientras el país esté restringido: el alta y los pagos pueden fallar. | | `data[].country` | texto | Sí | País, en código de 2 letras. Es el `country` del alta. | | `data[].currency` | texto | Sí | Moneda de la cuenta. Es el `currency` del alta. | | `data[].document_types` | lista de texto | Sí | Valores admitidos en `document_type`. | ###### Valores de `data[].account.account_types` | Valor | Significado | |---|---| | `checking` | Cuenta de cheques o corriente. | | `savings` | Cuenta de ahorro. | Lista vacía si el país no usa tipo de cuenta. ```json noexec="ejemplo" { "data": [ { "account": { "account_type_required": false, "account_types": [ "checking", "savings" ], "digits_only": true, "entity_required": true, "exact_length": null, "label": "Número de cuenta", "max_length": 20, "min_length": 5 }, "available": true, "country": "CL", "currency": "CLP", "document_types": [ "RUT", "PP" ] } ] } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) (sólo cuenta de prueba) | El encabezado `X-Fital-Scenario` trae un escenario desconocido. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`rail_not_enabled`](/docs/errores/#rail_not_enabled) | El riel `latam` no está habilitado para tu empresa. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 rail_not_enabled`: ```json noexec="ejemplo" { "error": { "code": "rail_not_enabled", "message": "Operación rechazada", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#rail_not_enabled" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/listar-paises-latam/). #### Guías - [Pago LatAm](/docs/recetas/pago-latam/) - [Cobertura](/docs/cobertura/) - [Autenticación](/docs/autenticacion/) - [Lista de migración](/docs/migracion/) - [Cuenta de prueba](/docs/sandbox/cuenta-de-prueba/) ### GET /v1/latam/countries/{country}/entities Bancos de un país del riel `latam` Bancos y entidades de un país del riel `latam`, con el valor a enviar en `entity`. Un país fuera de `GET /v1/latam/countries` responde `404`. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/latam/countries/{country}/entities` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `listLatamEntities` | | Acceso | Llave con el scope `beneficiaries:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | - Envía `code` tal cual en `entity`. `country` admite mayúsculas o minúsculas. - Consúltalo cuando `account.entity_required` sea `true`. - En la cuenta de prueba responde un solo banco ficticio. #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Parámetros | Parámetro | En | Tipo | Obligatorio | Descripción | |---|---|---|---|---| | `country` | ruta | texto | Sí | País, en código de 2 letras. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `data` | lista de objeto | Sí | Bancos y entidades del país. | | `data[].code` | texto | Sí | Valor a enviar en `entity`. | | `data[].name` | texto | Sí | Nombre del banco o entidad. | ```json noexec="ejemplo" { "data": [ { "code": "999", "name": "Banco de ejemplo" } ] } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`rail_not_enabled`](/docs/errores/#rail_not_enabled) | El riel `latam` no está habilitado para tu empresa. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 404 | [`not_found`](/docs/errores/#not_found) | El país no está en `GET /v1/latam/countries`. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 rail_not_enabled`: ```json noexec="ejemplo" { "error": { "code": "rail_not_enabled", "message": "Operación rechazada", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#rail_not_enabled" } } ``` `404 not_found`: ```json noexec="ejemplo" { "error": { "code": "not_found", "message": "No encontrado", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#not_found" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/listar-bancos-latam/). #### Guías - [Pago LatAm](/docs/recetas/pago-latam/) - [Cobertura](/docs/cobertura/) - [Autenticación](/docs/autenticacion/) - [Lista de migración](/docs/migracion/) - [Cuenta de prueba](/docs/sandbox/cuenta-de-prueba/) ## Pagos ### GET /v1/payments Pagos, con su último estado conocido Pagos creados por esta API, con su último estado conocido, del más nuevo al más viejo. Para el estado más fresco de uno, usa `GET /v1/payments/{id}`. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/payments` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `listPayments` | | Acceso | Llave con el scope `payments:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | - Es una vista guardada: no lee en vivo y no lleva `freshness`. Puede ir una vuelta por detrás del detalle. Ver [Lista y detalle](/docs/conceptos/#lista-y-detalle). - `failure_code` llega en `null` en esta lista, también con `failed`: léelo en `GET /v1/payments/{id}` o en `payment.failed`. #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Parámetros | Parámetro | En | Tipo | Obligatorio | Descripción | |---|---|---|---|---| | `limit` | consulta | texto | No | Filas por página, de 1 a 100. Por omisión, 25. | | `cursor` | consulta | texto | No | Valor de `next_cursor` de la página anterior. Hasta 1000 caracteres. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `data` | lista de objeto | Sí | Filas de esta página. | | `data[].amount` | texto o `null` | Sí | Monto pedido. | | `data[].beneficiary_id` | texto o `null` | Sí | Beneficiario del pago. | | `data[].created_at` | texto o `null` | Sí | Fecha de creación. ISO 8601 en UTC. | | `data[].currency` | texto o `null` | Sí | Moneda pedida. | | `data[].external_ref` | texto o `null` | Sí | Tu referencia. | | `data[].failure_code` | texto o `null` | Sí | Código del catálogo de errores si el pago falló, null en cualquier otro estado. En `GET /v1/payments` siempre es null: léelo en `GET /v1/payments/{id}` o en `payment.failed`. Trata la lista como extensible. `payment_rejected`: Rechazado sin un motivo más específico. `insufficient_funds`: Saldo insuficiente al ejecutar. `limit_exceeded`: Se superó el tope acumulado de la empresa o un límite del riel. `amount_below_minimum`: El monto quedó por debajo del mínimo aplicable. `account_restricted`: La cuenta tiene una restricción. `in_review`: Una revisión detuvo la ejecución. `quote_changed`: El débito superaría lo permitido. `quote_expired`: La cotización venció antes de ejecutar. | | `data[].final_amount` | texto o `null` | Sí | Monto final, cuando se conoce. | | `data[].final_currency` | texto o `null` | Sí | Moneda del monto final. | | `data[].freshness` | objeto | No | De dónde sale `status`. Sólo en `GET /v1/payments/{id}`. | | `data[].freshness.as_of` | texto o `null` | Sí | Hora de esa lectura, de la última que se aplicó o de la cancelación confirmada. Null si aún no hay ninguna. ISO 8601 en UTC. | | `data[].freshness.status` | texto | Sí | De dónde sale `status`. `current`: `status` sale de una lectura de ahora. `delayed`: La lectura no concluyó y `status` es lo último que se sabe. No lo tomes como un resultado nuevo. | | `data[].id` | texto | Sí | Identificador del pago, con prefijo `pay_`. | | `data[].metadata` | objeto | Sí | Los pares que enviaste al crear el pago. | | `data[].reference` | texto o `null` | Sí | Concepto del pago. | | `data[].status` | texto | Sí | Estado público. `processing`: Aceptado y en curso, o aún sin confirmar. Consúltalo hasta un estado terminal. `in_review`: En revisión. No lo repitas: el pago ya existe. `completed`: Se pagó. Aún puede pasar a `returned`. `failed`: No se hizo y no queda dinero en tránsito. `returned`: Se devolvió y el saldo ya volvió. `canceled`: Se canceló antes de ejecutarse. | | `has_more` | booleano | Sí | true si hay más filas después de esta página. | | `next_cursor` | texto o `null` | Sí | Cursor de la página siguiente. null si no hay. | ###### Valores de `data[].failure_code` | Valor | Significado | |---|---| | `payment_rejected` | Rechazado sin un motivo más específico. | | `insufficient_funds` | Saldo insuficiente al ejecutar. | | `limit_exceeded` | Se superó el tope acumulado de la empresa o un límite del riel. | | `amount_below_minimum` | El monto quedó por debajo del mínimo aplicable. | | `account_restricted` | La cuenta tiene una restricción. | | `in_review` | Una revisión detuvo la ejecución. | | `quote_changed` | El débito superaría lo permitido. | | `quote_expired` | La cotización venció antes de ejecutar. | Con `failed`, `GET /v1/payments/{id}` y `payment.failed` siempre traen un código: `payment_rejected` si no hay uno más específico. En cualquier otro estado, y en la lista `GET /v1/payments`, es `null`. Trata la lista como extensible. ###### Valores de `data[].freshness.status` | Valor | Significado | |---|---| | `current` | `status` sale de una lectura de ahora. | | `delayed` | La lectura no concluyó y `status` es lo último que se sabe. No lo tomes como un resultado nuevo. | ###### Valores de `data[].status` | Valor | Significado | |---|---| | `processing` | Aceptado y en curso, o aún sin confirmar. Consúltalo hasta un estado terminal. | | `in_review` | En revisión. No lo repitas: el pago ya existe. | | `completed` | Se pagó. Aún puede pasar a `returned`. | | `failed` | No se hizo y no queda dinero en tránsito. | | `returned` | Se devolvió y el saldo ya volvió. | | `canceled` | Se canceló antes de ejecutarse. | ```json noexec="ejemplo" { "data": [ { "amount": "1500.00", "beneficiary_id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "external_ref": "pedido-7731", "failure_code": null, "final_amount": "1500.00", "final_currency": "MXN", "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "metadata": {}, "reference": "Factura 1042", "status": "completed" } ], "has_more": false, "next_cursor": null } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "payments:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/listar-pagos/). #### Guías - [Conceptos](/docs/conceptos/) - [Paginación](/docs/paginacion/) - [Autenticación](/docs/autenticacion/) - [Conciliar](/docs/recetas/conciliar/) - [Pago masivo](/docs/recetas/pago-masivo/) - [Pago SPEI](/docs/recetas/pago-spei/) ### POST /v1/payments Crear un pago Crea un pago a un beneficiario aprobado. Exige `Idempotency-Key` y firma. Responde 202: el resultado llega por webhook o en `GET /v1/payments/{id}`. Si la respuesta se corta, repite con la misma clave: nunca crea un segundo pago. `in_review` con `retryable: false` quedó registrado: esa clave repite el rechazo, y volver a intentarlo exige otra. | Dato | Valor | |---|---| | Método y ruta | `POST /v1/payments` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `createPayment` | | Acceso | Llave con el scope `payments:write` | | Firma de petición | **Obligatoria** en producción y en la cuenta de prueba. El simulador público no la valida. Ver [Firma de petición](/docs/autenticacion/#firma-de-petición) | | Idempotencia | **Obligatoria**, con `Idempotency-Key`. Ver [Idempotencia](/docs/idempotencia/) | | Escenarios | `payment_completed`, `insufficient_funds`, `payment_held`. Ver [Ambientes](/docs/ambientes/#escenarios) | - El riel sale del beneficiario: no se elige aquí. - `quote_id` es opcional. Si lo envías, debe ser de este mismo beneficiario, monto y moneda, y `max_source_amount` manda sólo si es menor que lo cotizado. - `202` con `processing` no es fallo: el resultado llega por webhook o en `GET /v1/payments/{id}`. El encabezado `Location` trae la ruta del pago. #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `Content-Type` | Sí | `application/json`. | | `Idempotency-Key` | Sí | Clave única de esta operación, de 8 a 200 caracteres. Repetirla con el mismo cuerpo devuelve la misma respuesta durante 7 días. Con otro cuerpo da `422 idempotency_key_reused`, también pasado ese plazo. | | `X-Fital-Timestamp` | Sí | Hora Unix en segundos. Se acepta hasta 300 s de diferencia. | | `X-Fital-Signature` | Sí | Firma `v1=` de la petición con el secreto de firma de la llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Cuerpo | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `amount` | texto | Sí | Monto a enviar, como decimal en texto. | | `beneficiary_id` | texto | Sí | Beneficiario aprobado que recibe el pago. | | `currency` | texto | Sí | Moneda del monto. | | `external_ref` | texto | Sí | Tu referencia del pago, hasta 120 caracteres imprimibles. Vuelve en el pago y en sus eventos. Hasta 120 caracteres. | | `max_source_amount` | texto | No | Máximo a debitar. Con cotización, manda el menor de los dos. | | `metadata` | objeto | No | Hasta 20 pares clave y valor tuyos. Valores de hasta 500 caracteres. | | `quote_id` | texto | No | Cotización vigente de este mismo beneficiario, monto y moneda. Fija el tope de débito. | | `reference` | texto | No | Concepto del pago, hasta 140 caracteres imprimibles. Hasta 140 caracteres. | ```json noexec="ejemplo" { "beneficiary_id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "amount": "1500.00", "currency": "MXN", "external_ref": "factura-1042" } ``` #### Respuestas correctas ##### 202 Aceptado: el resultado final llega después | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `amount` | texto o `null` | Sí | Monto pedido. | | `beneficiary_id` | texto o `null` | Sí | Beneficiario del pago. | | `created_at` | texto o `null` | Sí | Fecha de creación. ISO 8601 en UTC. | | `currency` | texto o `null` | Sí | Moneda pedida. | | `external_ref` | texto o `null` | Sí | Tu referencia. | | `failure_code` | texto o `null` | Sí | Código del catálogo de errores si el pago falló, null en cualquier otro estado. En `GET /v1/payments` siempre es null: léelo en `GET /v1/payments/{id}` o en `payment.failed`. Trata la lista como extensible. `payment_rejected`: Rechazado sin un motivo más específico. `insufficient_funds`: Saldo insuficiente al ejecutar. `limit_exceeded`: Se superó el tope acumulado de la empresa o un límite del riel. `amount_below_minimum`: El monto quedó por debajo del mínimo aplicable. `account_restricted`: La cuenta tiene una restricción. `in_review`: Una revisión detuvo la ejecución. `quote_changed`: El débito superaría lo permitido. `quote_expired`: La cotización venció antes de ejecutar. | | `final_amount` | texto o `null` | Sí | Monto final, cuando se conoce. | | `final_currency` | texto o `null` | Sí | Moneda del monto final. | | `freshness` | objeto | No | De dónde sale `status`. Sólo en `GET /v1/payments/{id}`. | | `freshness.as_of` | texto o `null` | Sí | Hora de esa lectura, de la última que se aplicó o de la cancelación confirmada. Null si aún no hay ninguna. ISO 8601 en UTC. | | `freshness.status` | texto | Sí | De dónde sale `status`. `current`: `status` sale de una lectura de ahora. `delayed`: La lectura no concluyó y `status` es lo último que se sabe. No lo tomes como un resultado nuevo. | | `id` | texto | Sí | Identificador del pago, con prefijo `pay_`. | | `metadata` | objeto | Sí | Los pares que enviaste al crear el pago. | | `reference` | texto o `null` | Sí | Concepto del pago. | | `status` | texto | Sí | Estado público. `processing`: Aceptado y en curso, o aún sin confirmar. Consúltalo hasta un estado terminal. `in_review`: En revisión. No lo repitas: el pago ya existe. `completed`: Se pagó. Aún puede pasar a `returned`. `failed`: No se hizo y no queda dinero en tránsito. `returned`: Se devolvió y el saldo ya volvió. `canceled`: Se canceló antes de ejecutarse. | ###### Valores de `failure_code` | Valor | Significado | |---|---| | `payment_rejected` | Rechazado sin un motivo más específico. | | `insufficient_funds` | Saldo insuficiente al ejecutar. | | `limit_exceeded` | Se superó el tope acumulado de la empresa o un límite del riel. | | `amount_below_minimum` | El monto quedó por debajo del mínimo aplicable. | | `account_restricted` | La cuenta tiene una restricción. | | `in_review` | Una revisión detuvo la ejecución. | | `quote_changed` | El débito superaría lo permitido. | | `quote_expired` | La cotización venció antes de ejecutar. | Con `failed`, `GET /v1/payments/{id}` y `payment.failed` siempre traen un código: `payment_rejected` si no hay uno más específico. En cualquier otro estado, y en la lista `GET /v1/payments`, es `null`. Trata la lista como extensible. ###### Valores de `freshness.status` | Valor | Significado | |---|---| | `current` | `status` sale de una lectura de ahora. | | `delayed` | La lectura no concluyó y `status` es lo último que se sabe. No lo tomes como un resultado nuevo. | ###### Valores de `status` | Valor | Significado | |---|---| | `processing` | Aceptado y en curso, o aún sin confirmar. Consúltalo hasta un estado terminal. | | `in_review` | En revisión. No lo repitas: el pago ya existe. | | `completed` | Se pagó. Aún puede pasar a `returned`. | | `failed` | No se hizo y no queda dinero en tránsito. | | `returned` | Se devolvió y el saldo ya volvió. | | `canceled` | Se canceló antes de ejecutarse. | ```json noexec="ejemplo" { "amount": "1500.00", "beneficiary_id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "external_ref": "pedido-7731", "failure_code": null, "final_amount": null, "final_currency": null, "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "metadata": {}, "reference": "Factura 1042", "status": "processing" } ``` Variante `payment_completed`: ```json noexec="ejemplo" { "amount": "1500.00", "beneficiary_id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "external_ref": "pedido-7731", "failure_code": null, "final_amount": null, "final_currency": null, "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "metadata": {}, "reference": "Factura 1042", "status": "processing" } ``` Variante `payment_failed`: ```json noexec="ejemplo" { "amount": "1500.00", "beneficiary_id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "external_ref": "pedido-7731", "failure_code": null, "final_amount": null, "final_currency": null, "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "metadata": {}, "reference": "Factura 1042", "status": "processing" } ``` Variante `payment_returned`: ```json noexec="ejemplo" { "amount": "1500.00", "beneficiary_id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "external_ref": "pedido-7731", "failure_code": null, "final_amount": null, "final_currency": null, "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "metadata": {}, "reference": "Factura 1042", "status": "processing" } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Idempotent-Replayed` | `true` si la respuesta repite una anterior con esa clave. | | `Location` | Ruta del recurso, con `201` y `202`. | | `Retry-After` | Segundos a esperar, con `429`, con `409 idempotency_in_progress` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún campo no cumple el esquema. También si `amount` es cero, si lleva decimales en una moneda sin decimales, o si `quote_id` es de otra operación. | `false` | | 400 | [`idempotency_key_required`](/docs/errores/#idempotency_key_required) | Falta `Idempotency-Key`. | `false` | | 400 | [`idempotency_key_invalid`](/docs/errores/#idempotency_key_invalid) | La clave no mide entre 8 y 200 caracteres, o viene duplicada. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 401 | [`invalid_signature`](/docs/errores/#invalid_signature) | Falta la firma, no coincide o tiene más de 5 minutos. | `false` | | 403 | [`rail_not_enabled`](/docs/errores/#rail_not_enabled) | El riel del beneficiario no está habilitado para tu empresa. | `false` | | 403 | [`not_enabled`](/docs/errores/#not_enabled) | PIX sin activación vigente para tu empresa. | `false` | | 403 | [`account_restricted`](/docs/errores/#account_restricted) | La cuenta tiene una restricción. Puede llegar antes de ejecutar, o quedar registrado al ejecutar. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 404 | [`not_found`](/docs/errores/#not_found) | El beneficiario o la cotización no existen, o son de otra empresa. | `false` | | 409 | [`payment_in_flight`](/docs/errores/#payment_in_flight) | Otra salida de dinero de tu empresa sigue sin confirmarse. | `true` | | 409 | [`quote_changed`](/docs/errores/#quote_changed) | El débito superaría `max_source_amount` o lo cotizado. No se creó ningún pago. | `false` | | 409 | [`quote_expired`](/docs/errores/#quote_expired) | La cotización `quote_id` venció. | `false` | | 409 | [`idempotency_in_progress`](/docs/errores/#idempotency_in_progress) | Una petición anterior con esa clave sigue en curso. Llega con `Retry-After`. | `true` | | 413 | [`invalid_request`](/docs/errores/#invalid_request) | El cuerpo supera 64 KB. | `false` | | 422 | [`beneficiary_not_approved`](/docs/errores/#beneficiary_not_approved) | El beneficiario existe pero no está `approved`. | `true` | | 422 | [`rail_unavailable`](/docs/errores/#rail_unavailable) | El riel no está disponible por ahora. | `true` | | 422 | [`currency_not_supported`](/docs/errores/#currency_not_supported) | `currency` no es la moneda del riel del beneficiario. | `false` | | 422 | [`in_review`](/docs/errores/#in_review) | Una revisión frena la operación. Con `retryable: true` se frenó antes de ejecutar y no hay `pay_`. Con `retryable: false` quedó registrado: el pago queda `failed` con `failure_code: in_review`. | `true` | | 422 | [`insufficient_funds`](/docs/errores/#insufficient_funds) | Saldo insuficiente. El rechazo queda registrado con esa clave. | `false` | | 422 | [`limit_exceeded`](/docs/errores/#limit_exceeded) | Se superó un límite del riel. El rechazo queda registrado con esa clave. | `false` | | 422 | [`amount_below_minimum`](/docs/errores/#amount_below_minimum) | El monto está por debajo del mínimo aplicable. El rechazo queda registrado con esa clave. | `false` | | 422 | [`payment_rejected`](/docs/errores/#payment_rejected) | El pago se rechazó sin un motivo más específico. El rechazo queda registrado con esa clave. | `false` | | 422 | [`idempotency_key_reused`](/docs/errores/#idempotency_key_reused) | Usaste esa clave con otro cuerpo, también pasados 7 días. | `false` | | 422 | [`invalid_beneficiary`](/docs/errores/#invalid_beneficiary) (sólo cuenta de prueba) | El beneficiario se dio de baja, o usaste el escenario `invalid_beneficiary`. | `false` | | 422 | [`limit_exceeded`](/docs/errores/#limit_exceeded) (sólo cuenta de prueba) | El monto supera 1,000,000 ficticios por pago. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 429 | [`sandbox_limit`](/docs/errores/#sandbox_limit) (sólo cuenta de prueba) | Alcanzaste 100 pagos y cobros de hoy. | `false` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "amount": "Valor inválido" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 rail_not_enabled`: ```json noexec="ejemplo" { "error": { "code": "rail_not_enabled", "message": "Operación rechazada", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#rail_not_enabled" } } ``` `404 not_found`: ```json noexec="ejemplo" { "error": { "code": "not_found", "message": "No encontrado", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#not_found" } } ``` `409 payment_in_flight`: ```json noexec="ejemplo" { "error": { "code": "payment_in_flight", "message": "Operación rechazada", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#payment_in_flight" } } ``` `413 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `422 beneficiary_not_approved`: ```json noexec="ejemplo" { "error": { "code": "beneficiary_not_approved", "message": "Operación rechazada", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#beneficiary_not_approved" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Ante un timeout o una conexión cortada, repite con la misma clave: nunca crea un segundo pago. Un error con `retryable: false` quedó registrado o no se arregla repitiendo: la misma clave repite ese rechazo, así que corrige la causa y usa una clave nueva. Ver [Qué hacer con la clave tras un rechazo](/docs/idempotencia/#qué-hacer-con-la-clave-tras-un-rechazo). - `409 payment_in_flight`: Repite con la misma clave cuando la otra salida se resuelva. - `409 idempotency_in_progress`: Espera `Retry-After` y repite con la misma clave. - `422 beneficiary_not_approved`: Apruébalo en el panel y repite con la misma clave. - `422 rail_unavailable`: Repite más tarde. - `422 in_review`: Mira `retryable`. Con `true`, repite con la misma clave cuando la revisión se levante. Con `false`, el rechazo quedó registrado y esa clave lo repite: usa una clave nueva tras la revisión. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/crear-pago/). #### Guías - [Tu primer pago de prueba](/docs/primer-pago/) - [Pago SPEI](/docs/recetas/pago-spei/) - [Pago masivo](/docs/recetas/pago-masivo/) - [Reintentos](/docs/recetas/reintentos/) - [Idempotencia](/docs/idempotencia/) - [Autenticación](/docs/autenticacion/) - [Pago ACH](/docs/recetas/pago-ach/) - [Pago Bre-B](/docs/recetas/pago-bre-b/) - [Pago FPS](/docs/recetas/pago-fps/) - [Pago LatAm](/docs/recetas/pago-latam/) - [Pago PIX](/docs/recetas/pago-pix/) - [Pago SEPA](/docs/recetas/pago-sepa/) - [Pago wire](/docs/recetas/pago-wire/) ### GET /v1/payments/{id} Un pago Un pago de tu empresa. `completed` exige confirmación del riel. `freshness` dice de dónde sale el estado: con `current` es la lectura de ahora y `as_of` su hora; con `delayed` no hubo lectura concluyente y es lo último que se sabe, a la hora de `as_of`. Una cancelación confirmada responde `canceled` con `delayed` hasta que una lectura la confirme. Si un pago cancelado vuelve a `processing` con `current`, se reabrió de verdad. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/payments/{id}` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `getPayment` | | Acceso | Llave con el scope `payments:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | - `freshness` sólo existe aquí. Con `delayed`, consulta otra vez más tarde. Ver [Frescura del detalle](/docs/conceptos/#frescura-del-detalle). - Con `failed`, `failure_code` siempre trae un código: `payment_rejected` si no hay uno más específico o si el detalle ya no se puede leer. En cualquier otro estado es `null`. #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Parámetros | Parámetro | En | Tipo | Obligatorio | Descripción | |---|---|---|---|---| | `id` | ruta | texto | Sí | Identificador del recurso. Hasta 250 caracteres. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `amount` | texto o `null` | Sí | Monto pedido. | | `beneficiary_id` | texto o `null` | Sí | Beneficiario del pago. | | `created_at` | texto o `null` | Sí | Fecha de creación. ISO 8601 en UTC. | | `currency` | texto o `null` | Sí | Moneda pedida. | | `external_ref` | texto o `null` | Sí | Tu referencia. | | `failure_code` | texto o `null` | Sí | Código del catálogo de errores si el pago falló, null en cualquier otro estado. En `GET /v1/payments` siempre es null: léelo en `GET /v1/payments/{id}` o en `payment.failed`. Trata la lista como extensible. `payment_rejected`: Rechazado sin un motivo más específico. `insufficient_funds`: Saldo insuficiente al ejecutar. `limit_exceeded`: Se superó el tope acumulado de la empresa o un límite del riel. `amount_below_minimum`: El monto quedó por debajo del mínimo aplicable. `account_restricted`: La cuenta tiene una restricción. `in_review`: Una revisión detuvo la ejecución. `quote_changed`: El débito superaría lo permitido. `quote_expired`: La cotización venció antes de ejecutar. | | `final_amount` | texto o `null` | Sí | Monto final, cuando se conoce. | | `final_currency` | texto o `null` | Sí | Moneda del monto final. | | `freshness` | objeto | No | De dónde sale `status`. Sólo en `GET /v1/payments/{id}`. | | `freshness.as_of` | texto o `null` | Sí | Hora de esa lectura, de la última que se aplicó o de la cancelación confirmada. Null si aún no hay ninguna. ISO 8601 en UTC. | | `freshness.status` | texto | Sí | De dónde sale `status`. `current`: `status` sale de una lectura de ahora. `delayed`: La lectura no concluyó y `status` es lo último que se sabe. No lo tomes como un resultado nuevo. | | `id` | texto | Sí | Identificador del pago, con prefijo `pay_`. | | `metadata` | objeto | Sí | Los pares que enviaste al crear el pago. | | `reference` | texto o `null` | Sí | Concepto del pago. | | `status` | texto | Sí | Estado público. `processing`: Aceptado y en curso, o aún sin confirmar. Consúltalo hasta un estado terminal. `in_review`: En revisión. No lo repitas: el pago ya existe. `completed`: Se pagó. Aún puede pasar a `returned`. `failed`: No se hizo y no queda dinero en tránsito. `returned`: Se devolvió y el saldo ya volvió. `canceled`: Se canceló antes de ejecutarse. | ###### Valores de `failure_code` | Valor | Significado | |---|---| | `payment_rejected` | Rechazado sin un motivo más específico. | | `insufficient_funds` | Saldo insuficiente al ejecutar. | | `limit_exceeded` | Se superó el tope acumulado de la empresa o un límite del riel. | | `amount_below_minimum` | El monto quedó por debajo del mínimo aplicable. | | `account_restricted` | La cuenta tiene una restricción. | | `in_review` | Una revisión detuvo la ejecución. | | `quote_changed` | El débito superaría lo permitido. | | `quote_expired` | La cotización venció antes de ejecutar. | Con `failed`, `GET /v1/payments/{id}` y `payment.failed` siempre traen un código: `payment_rejected` si no hay uno más específico. En cualquier otro estado, y en la lista `GET /v1/payments`, es `null`. Trata la lista como extensible. ###### Valores de `freshness.status` | Valor | Significado | |---|---| | `current` | `status` sale de una lectura de ahora. | | `delayed` | La lectura no concluyó y `status` es lo último que se sabe. No lo tomes como un resultado nuevo. | ###### Valores de `status` | Valor | Significado | |---|---| | `processing` | Aceptado y en curso, o aún sin confirmar. Consúltalo hasta un estado terminal. | | `in_review` | En revisión. No lo repitas: el pago ya existe. | | `completed` | Se pagó. Aún puede pasar a `returned`. | | `failed` | No se hizo y no queda dinero en tránsito. | | `returned` | Se devolvió y el saldo ya volvió. | | `canceled` | Se canceló antes de ejecutarse. | ```json noexec="ejemplo" { "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "status": "processing", "beneficiary_id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "amount": "1500.00", "currency": "MXN", "final_amount": null, "final_currency": null, "reference": "Factura 1042", "external_ref": "factura-1042", "metadata": {}, "failure_code": null, "created_at": "2026-11-04T18:00:00.000Z", "freshness": { "status": "current", "as_of": "2026-11-04T18:00:00.000Z" } } ``` `processing` con `delayed`: la lectura no concluyó: ```json noexec="ejemplo" { "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "status": "processing", "beneficiary_id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "amount": "1500.00", "currency": "MXN", "final_amount": null, "final_currency": null, "reference": "Factura 1042", "external_ref": "factura-1042", "metadata": {}, "failure_code": null, "created_at": "2026-11-04T18:00:00.000Z", "freshness": { "status": "delayed", "as_of": "2026-11-04T18:00:30.000Z" } } ``` En revisión: no lo repitas: ```json noexec="ejemplo" { "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "status": "in_review", "beneficiary_id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "amount": "1500.00", "currency": "MXN", "final_amount": null, "final_currency": null, "reference": "Factura 1042", "external_ref": "factura-1042", "metadata": {}, "failure_code": null, "created_at": "2026-11-04T18:00:00.000Z", "freshness": { "status": "current", "as_of": "2026-11-04T18:00:00.000Z" } } ``` `failed`, con su código: ```json noexec="ejemplo" { "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "status": "failed", "beneficiary_id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "amount": "1500.00", "currency": "MXN", "final_amount": null, "final_currency": null, "reference": "Factura 1042", "external_ref": "factura-1042", "metadata": {}, "failure_code": "insufficient_funds", "created_at": "2026-11-04T18:00:00.000Z", "freshness": { "status": "current", "as_of": "2026-11-04T18:00:00.000Z" } } ``` `returned`: se devolvió después de `completed`: ```json noexec="ejemplo" { "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "status": "returned", "beneficiary_id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "amount": "1500.00", "currency": "MXN", "final_amount": null, "final_currency": null, "reference": "Factura 1042", "external_ref": "factura-1042", "metadata": {}, "failure_code": null, "created_at": "2026-11-04T18:00:00.000Z", "freshness": { "status": "current", "as_of": "2026-11-04T18:00:00.000Z" } } ``` `canceled` con `delayed`: falta la lectura que lo confirme: ```json noexec="ejemplo" { "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "status": "canceled", "beneficiary_id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "amount": "1500.00", "currency": "MXN", "final_amount": null, "final_currency": null, "reference": "Factura 1042", "external_ref": "factura-1042", "metadata": {}, "failure_code": null, "created_at": "2026-11-04T18:00:00.000Z", "freshness": { "status": "delayed", "as_of": "2026-11-04T18:01:30.000Z" } } ``` Variante `payment_completed`: ```json noexec="ejemplo" { "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "status": "completed", "beneficiary_id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "amount": "1500.00", "currency": "MXN", "final_amount": "1500.00", "final_currency": "MXN", "reference": "Factura 1042", "external_ref": "factura-1042", "metadata": {}, "failure_code": null, "created_at": "2026-11-04T18:00:00.000Z", "freshness": { "status": "current", "as_of": "2026-11-04T18:00:00.000Z" } } ``` Variante `payment_failed`: ```json noexec="ejemplo" { "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "status": "failed", "beneficiary_id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "amount": "1500.00", "currency": "MXN", "final_amount": null, "final_currency": null, "reference": "Factura 1042", "external_ref": "factura-1042", "metadata": {}, "failure_code": "payment_rejected", "created_at": "2026-11-04T18:00:00.000Z", "freshness": { "status": "current", "as_of": "2026-11-04T18:00:00.000Z" } } ``` Variante `payment_returned`: ```json noexec="ejemplo" { "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "status": "returned", "beneficiary_id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "amount": "1500.00", "currency": "MXN", "final_amount": null, "final_currency": null, "reference": "Factura 1042", "external_ref": "factura-1042", "metadata": {}, "failure_code": null, "created_at": "2026-11-04T18:00:00.000Z", "freshness": { "status": "current", "as_of": "2026-11-04T18:00:00.000Z" } } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 404 | [`not_found`](/docs/errores/#not_found) | El pago no existe o es de otra empresa. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "payments:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `404 not_found`: ```json noexec="ejemplo" { "error": { "code": "not_found", "message": "No encontrado", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#not_found" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/consultar-pago/). #### Guías - [Conceptos](/docs/conceptos/) - [Conciliar](/docs/recetas/conciliar/) - [Reintentos](/docs/recetas/reintentos/) - [Ambientes](/docs/ambientes/) - [Autenticación](/docs/autenticacion/) ### POST /v1/payments/{id}/cancel Cancelar un pago que aún espera fondos Cancela un pago que aún espera fondos. Exige `Idempotency-Key` y firma. Si ya no se puede cancelar da `409 not_cancellable`. 202 si la cancelación aún no se confirma: repite con la misma clave. | Dato | Valor | |---|---| | Método y ruta | `POST /v1/payments/{id}/cancel` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `cancelPayment` | | Acceso | Llave con el scope `payments:write` | | Firma de petición | **Obligatoria** en producción y en la cuenta de prueba. El simulador público no la valida. Ver [Firma de petición](/docs/autenticacion/#firma-de-petición) | | Idempotencia | **Obligatoria**, con `Idempotency-Key`. Ver [Idempotencia](/docs/idempotencia/) | - Sin cuerpo: firma el hash de cero bytes. - Tras `200`, `GET /v1/payments/{id}` responde `canceled` con `freshness.status` en `delayed` hasta que una lectura lo confirme. #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `Idempotency-Key` | Sí | Clave única de esta operación, de 8 a 200 caracteres. Repetirla con el mismo cuerpo devuelve la misma respuesta durante 7 días. Con otro cuerpo da `422 idempotency_key_reused`, también pasado ese plazo. | | `X-Fital-Timestamp` | Sí | Hora Unix en segundos. Se acepta hasta 300 s de diferencia. | | `X-Fital-Signature` | Sí | Firma `v1=` de la petición con el secreto de firma de la llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Parámetros | Parámetro | En | Tipo | Obligatorio | Descripción | |---|---|---|---|---| | `id` | ruta | texto | Sí | Identificador del recurso. Hasta 250 caracteres. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `amount` | texto o `null` | Sí | Monto pedido. | | `beneficiary_id` | texto o `null` | Sí | Beneficiario del pago. | | `created_at` | texto o `null` | Sí | Fecha de creación. ISO 8601 en UTC. | | `currency` | texto o `null` | Sí | Moneda pedida. | | `external_ref` | texto o `null` | Sí | Tu referencia. | | `failure_code` | texto o `null` | Sí | Código del catálogo de errores si el pago falló, null en cualquier otro estado. En `GET /v1/payments` siempre es null: léelo en `GET /v1/payments/{id}` o en `payment.failed`. Trata la lista como extensible. `payment_rejected`: Rechazado sin un motivo más específico. `insufficient_funds`: Saldo insuficiente al ejecutar. `limit_exceeded`: Se superó el tope acumulado de la empresa o un límite del riel. `amount_below_minimum`: El monto quedó por debajo del mínimo aplicable. `account_restricted`: La cuenta tiene una restricción. `in_review`: Una revisión detuvo la ejecución. `quote_changed`: El débito superaría lo permitido. `quote_expired`: La cotización venció antes de ejecutar. | | `final_amount` | texto o `null` | Sí | Monto final, cuando se conoce. | | `final_currency` | texto o `null` | Sí | Moneda del monto final. | | `freshness` | objeto | No | De dónde sale `status`. Sólo en `GET /v1/payments/{id}`. | | `freshness.as_of` | texto o `null` | Sí | Hora de esa lectura, de la última que se aplicó o de la cancelación confirmada. Null si aún no hay ninguna. ISO 8601 en UTC. | | `freshness.status` | texto | Sí | De dónde sale `status`. `current`: `status` sale de una lectura de ahora. `delayed`: La lectura no concluyó y `status` es lo último que se sabe. No lo tomes como un resultado nuevo. | | `id` | texto | Sí | Identificador del pago, con prefijo `pay_`. | | `metadata` | objeto | Sí | Los pares que enviaste al crear el pago. | | `reference` | texto o `null` | Sí | Concepto del pago. | | `status` | texto | Sí | Estado público. `processing`: Aceptado y en curso, o aún sin confirmar. Consúltalo hasta un estado terminal. `in_review`: En revisión. No lo repitas: el pago ya existe. `completed`: Se pagó. Aún puede pasar a `returned`. `failed`: No se hizo y no queda dinero en tránsito. `returned`: Se devolvió y el saldo ya volvió. `canceled`: Se canceló antes de ejecutarse. | ###### Valores de `failure_code` | Valor | Significado | |---|---| | `payment_rejected` | Rechazado sin un motivo más específico. | | `insufficient_funds` | Saldo insuficiente al ejecutar. | | `limit_exceeded` | Se superó el tope acumulado de la empresa o un límite del riel. | | `amount_below_minimum` | El monto quedó por debajo del mínimo aplicable. | | `account_restricted` | La cuenta tiene una restricción. | | `in_review` | Una revisión detuvo la ejecución. | | `quote_changed` | El débito superaría lo permitido. | | `quote_expired` | La cotización venció antes de ejecutar. | Con `failed`, `GET /v1/payments/{id}` y `payment.failed` siempre traen un código: `payment_rejected` si no hay uno más específico. En cualquier otro estado, y en la lista `GET /v1/payments`, es `null`. Trata la lista como extensible. ###### Valores de `freshness.status` | Valor | Significado | |---|---| | `current` | `status` sale de una lectura de ahora. | | `delayed` | La lectura no concluyó y `status` es lo último que se sabe. No lo tomes como un resultado nuevo. | ###### Valores de `status` | Valor | Significado | |---|---| | `processing` | Aceptado y en curso, o aún sin confirmar. Consúltalo hasta un estado terminal. | | `in_review` | En revisión. No lo repitas: el pago ya existe. | | `completed` | Se pagó. Aún puede pasar a `returned`. | | `failed` | No se hizo y no queda dinero en tránsito. | | `returned` | Se devolvió y el saldo ya volvió. | | `canceled` | Se canceló antes de ejecutarse. | ```json noexec="ejemplo" { "amount": "1500.00", "beneficiary_id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "external_ref": "pedido-7731", "failure_code": null, "final_amount": null, "final_currency": null, "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "metadata": {}, "reference": "Factura 1042", "status": "canceled" } ``` ##### 202 Aceptado: el resultado final llega después Misma forma que la respuesta anterior. ```json noexec="ejemplo" { "amount": "1500.00", "beneficiary_id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "external_ref": "pedido-7731", "failure_code": null, "final_amount": null, "final_currency": null, "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "metadata": {}, "reference": "Factura 1042", "status": "processing" } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Idempotent-Replayed` | `true` si la respuesta repite una anterior con esa clave. | | `Retry-After` | Segundos a esperar, con `429`, con `409 idempotency_in_progress` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 400 | [`idempotency_key_required`](/docs/errores/#idempotency_key_required) | Falta `Idempotency-Key`. | `false` | | 400 | [`idempotency_key_invalid`](/docs/errores/#idempotency_key_invalid) | La clave no mide entre 8 y 200 caracteres, o viene duplicada. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 401 | [`invalid_signature`](/docs/errores/#invalid_signature) | Falta la firma, no coincide o tiene más de 5 minutos. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 404 | [`not_found`](/docs/errores/#not_found) | El pago no existe o es de otra empresa. | `false` | | 409 | [`not_cancellable`](/docs/errores/#not_cancellable) | El pago ya no se puede cancelar. Es la respuesta normal cuando ya avanzó. Un pago `latam` nunca se cancela por esta operación. | `false` | | 409 | [`idempotency_in_progress`](/docs/errores/#idempotency_in_progress) | Una petición anterior con esa clave sigue en curso. Llega con `Retry-After`. | `true` | | 422 | [`idempotency_key_reused`](/docs/errores/#idempotency_key_reused) | Usaste esa clave con otro cuerpo, también pasados 7 días. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "payments:write" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `404 not_found`: ```json noexec="ejemplo" { "error": { "code": "not_found", "message": "No encontrado", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#not_found" } } ``` `409 not_cancellable`: ```json noexec="ejemplo" { "error": { "code": "not_cancellable", "message": "El pago no se puede cancelar", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#not_cancellable" } } ``` `422 idempotency_key_reused`: ```json noexec="ejemplo" { "error": { "code": "idempotency_key_reused", "message": "La clave ya se usó con otro cuerpo", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#idempotency_key_reused" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos `202` significa que la cancelación aún no se confirma: repite con la misma clave. Ante un timeout, repite con la misma clave: una cancelación nunca se da por aceptada. - `409 idempotency_in_progress`: Espera `Retry-After` y repite con la misma clave. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/cancelar-pago/). #### Guías - [Pago SPEI](/docs/recetas/pago-spei/) - [Conceptos](/docs/conceptos/) - [Autenticación](/docs/autenticacion/) - [Pago LatAm](/docs/recetas/pago-latam/) ### POST /v1/quotes Cotización indicativa con vencimiento Estima cuánto se debitaría por un pago a un beneficiario de tu empresa. Es indicativa y vence. No mueve dinero. Envía `quote_id` al crear el pago para fijar el tope de débito. | Dato | Valor | |---|---| | Método y ruta | `POST /v1/quotes` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `createQuote` | | Acceso | Llave con el scope `quotes:write` | | Firma de petición | No aplica | | Idempotencia | No aplica | - Sin clave de idempotencia y sin firma: no mueve dinero. Repetirla crea otra cotización. - Vence a los 60 segundos en producción y a los 300 en la cuenta de prueba. Usa `expires_at`. - El beneficiario no necesita estar aprobado para cotizar: sí para pagar. #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `Content-Type` | Sí | `application/json`. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Cuerpo | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `amount` | texto | Sí | Monto a enviar. | | `beneficiary_id` | texto | Sí | Beneficiario del pago que quieres cotizar. | | `currency` | texto | Sí | Moneda del monto. | ```json noexec="ejemplo" { "beneficiary_id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "amount": "1500.00", "currency": "MXN" } ``` #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `amount` | texto | Sí | Monto a enviar. | | `beneficiary_id` | texto | Sí | Beneficiario cotizado. | | `currency` | texto | Sí | Moneda del monto. | | `expires_at` | texto | Sí | Vencimiento de la cotización. ISO 8601 en UTC. | | `id` | texto | Sí | Identificador de la cotización, con prefijo `quo_`. | | `indicative` | booleano | Sí | Siempre true: no es un precio garantizado. | | `source_amount` | texto | Sí | Débito estimado. | | `source_currency` | texto | Sí | Moneda del débito. | ```json noexec="ejemplo" { "amount": "1500.00", "beneficiary_id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "currency": "MXN", "expires_at": "2026-11-04T18:05:00.000Z", "id": "quo_8dZxN3vTk6LqB2sW9mHrY4cP", "indicative": true, "source_amount": "82.50", "source_currency": "USD" } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 404 | [`not_found`](/docs/errores/#not_found) | El beneficiario no existe o es de otra empresa. | `false` | | 413 | [`invalid_request`](/docs/errores/#invalid_request) | El cuerpo supera 64 KB. | `false` | | 422 | [`currency_not_supported`](/docs/errores/#currency_not_supported) | `currency` no es la moneda del riel del beneficiario. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | No se pudo calcular el débito. No se creó ninguna cotización. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "amount": "Valor inválido" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "quotes:write" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `404 not_found`: ```json noexec="ejemplo" { "error": { "code": "not_found", "message": "No encontrado", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#not_found" } } ``` `413 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `422 currency_not_supported`: ```json noexec="ejemplo" { "error": { "code": "currency_not_supported", "message": "Operación rechazada", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#currency_not_supported" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos No lleva clave de idempotencia y no mueve dinero: repetirla es seguro. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/crear-cotizacion/). #### Guías - [Conceptos](/docs/conceptos/) - [Pago SPEI](/docs/recetas/pago-spei/) - [Autenticación](/docs/autenticacion/) - [Pago ACH](/docs/recetas/pago-ach/) - [Pago LatAm](/docs/recetas/pago-latam/) ## Cobros ### GET /v1/collection-methods Métodos de cobro Métodos de cobro de la empresa, con país, moneda, mínimos y máximos. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/collection-methods` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `listCollectionMethods` | | Acceso | Llave con el scope `collections:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | - Sólo lista los métodos que puede usar tu empresa. `available: false` significa que el método existe pero hoy no se puede usar. - `payer_fields` lista lo que el método pide en `payer` al crear el cobro, y `POST /v1/collections` lo aplica: `required`, `options` con los valores admitidos y `phone_prefix`. Vacío significa que no pide nada. #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `data` | lista de objeto | Sí | Métodos de cobro disponibles para la empresa. | | `data[].available` | booleano | Sí | true si se puede usar ahora. | | `data[].country` | texto o `null` | Sí | País del método. | | `data[].currency` | texto o `null` | Sí | Moneda del método. | | `data[].max_amount` | texto o `null` | Sí | Monto máximo. | | `data[].method` | texto o `null` | Sí | Nombre a usar en `method`. | | `data[].min_amount` | texto o `null` | Sí | Monto mínimo. | | `data[].payer_fields` | lista de objeto | Sí | Lo que el método pide en `payer` al crear el cobro. | | `data[].payer_fields[].name` | texto | Sí | Clave dentro de `payer`. `firstName`: Nombre, o razón social. `lastName`: Apellido. `email`: Correo. `phone`: Teléfono con su prefijo internacional. `documentType`: Tipo de documento. `documentNumber`: Número de documento. | | `data[].payer_fields[].options` | lista de texto o `null` | Sí | Valores admitidos. null si es texto libre. | | `data[].payer_fields[].phone_prefix` | texto o `null` | Sí | Prefijo del país, como `+57`. null si no aplica. | | `data[].payer_fields[].required` | booleano | Sí | true si el cobro se rechaza sin este dato. | ###### Valores de `data[].payer_fields[].name` | Valor | Significado | |---|---| | `firstName` | Nombre, o razón social. | | `lastName` | Apellido. | | `email` | Correo. | | `phone` | Teléfono con su prefijo internacional. | | `documentType` | Tipo de documento. | | `documentNumber` | Número de documento. | El contrato lo declara como texto: trata la lista como extensible. ```json noexec="ejemplo" { "data": [ { "available": true, "country": "MX", "currency": "MXN", "max_amount": "50000.00", "method": "spei", "min_amount": "10.00", "payer_fields": [ { "name": "firstName", "options": null, "phone_prefix": null, "required": true }, { "name": "email", "options": null, "phone_prefix": null, "required": true }, { "name": "documentType", "options": [ "RFC", "CURP" ], "phone_prefix": null, "required": false }, { "name": "phone", "options": null, "phone_prefix": "+52", "required": false } ] } ] } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) (sólo cuenta de prueba) | El encabezado `X-Fital-Scenario` trae un escenario desconocido. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "collections:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/listar-metodos-de-cobro/). #### Guías - [Cobros](/docs/recetas/cobros/) - [Cobertura](/docs/cobertura/) - [Autenticación](/docs/autenticacion/) - [Lista de migración](/docs/migracion/) - [Paginación](/docs/paginacion/) ### GET /v1/collections Cobros, con su último estado conocido Cobros creados por esta API, con el último estado conocido, del más nuevo al más viejo. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/collections` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `listCollections` | | Acceso | Llave con el scope `collections:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Parámetros | Parámetro | En | Tipo | Obligatorio | Descripción | |---|---|---|---|---| | `limit` | consulta | texto | No | Filas por página, de 1 a 100. Por omisión, 25. | | `cursor` | consulta | texto | No | Valor de `next_cursor` de la página anterior. Hasta 1000 caracteres. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `data` | lista de objeto | Sí | Filas de esta página. | | `data[].amount` | texto o `null` | Sí | Monto que paga tu cliente. | | `data[].created_at` | texto o `null` | Sí | Fecha de creación. ISO 8601 en UTC. | | `data[].currency` | texto o `null` | Sí | Moneda del cobro. | | `data[].destination_amount` | texto o `null` | Sí | Monto que recibes. | | `data[].expires_at` | texto o `null` | Sí | Vencimiento del cobro. ISO 8601 en UTC. | | `data[].external_ref` | texto o `null` | Sí | Tu referencia. | | `data[].id` | texto | Sí | Identificador del cobro, con prefijo `col_`. | | `data[].instruction` | objeto o `null` | Sí | Cómo pagar. null mientras no esté lista. | | `data[].instruction.type` | texto o `null` | Sí | Tipo de instrucción. Depende del método: trata la lista como extensible. `LINK`: `value` es una dirección que se abre para pagar. `QR`: `value` se muestra o se copia, como un QR o un texto de pago. `REFERENCE`: `value` es la referencia que quien paga da en la tienda, en un cobro `efectivo`. `clabe`: `value` es una CLABE. Sólo en sandbox. | | `data[].instruction.value` | texto o `null` | Sí | Dato que usa quien paga. | | `data[].metadata` | objeto | Sí | Los pares que enviaste al crear el cobro. | | `data[].method` | texto o `null` | Sí | Método del cobro. | | `data[].status` | texto | Sí | Estado del cobro. `processing`: El alta aún no se confirma. `pending`: Espera el pago. `paid`: Pagado. No vuelve atrás. En `efectivo`, la tienda ya confirmó el pago y el abono a tu cuenta llega después. `expired`: Venció sin pago. No vuelve atrás. `failed`: No se pudo crear o completar. | | `has_more` | booleano | Sí | true si hay más filas después de esta página. | | `next_cursor` | texto o `null` | Sí | Cursor de la página siguiente. null si no hay. | ###### Valores de `data[].instruction.type` | Valor | Significado | |---|---| | `LINK` | `value` es una dirección que se abre para pagar. | | `QR` | `value` se muestra o se copia, como un QR o un texto de pago. | | `REFERENCE` | `value` es la referencia que quien paga da en la tienda, en un cobro `efectivo`. | | `clabe` | `value` es una CLABE. Sólo en la cuenta de prueba. | Depende del método. Trata la lista como extensible. ###### Valores de `data[].status` | Valor | Significado | |---|---| | `processing` | El alta aún no se confirma. | | `pending` | Espera el pago. | | `paid` | Pagado. No vuelve atrás. | | `expired` | Venció sin pago. No vuelve atrás. | | `failed` | No se pudo crear o completar. | ```json noexec="ejemplo" { "data": [ { "amount": "1250.00", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_amount": null, "expires_at": "2026-11-05T18:00:00.000Z", "external_ref": "pedido-7731", "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "instruction": { "type": "clabe", "value": "999180000000000022" }, "metadata": {}, "method": "spei", "status": "pending" } ], "has_more": false, "next_cursor": null } ``` Instrucción `clabe`: sólo en sandbox.: ```json noexec="ejemplo" { "data": [ { "amount": "1250.00", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_amount": null, "expires_at": "2026-11-05T18:00:00.000Z", "external_ref": "pedido-7731", "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "instruction": { "type": "clabe", "value": "999180000000000022" }, "metadata": {}, "method": "spei", "status": "pending" } ], "has_more": false, "next_cursor": null } ``` Instrucción `REFERENCE`: quien paga da `value` en la tienda.: ```json noexec="ejemplo" { "data": [ { "amount": "1250.00", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_amount": null, "expires_at": "2026-11-05T18:00:00.000Z", "external_ref": "pedido-7731", "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "instruction": { "type": "REFERENCE", "value": "999000000000000000000000000022" }, "metadata": {}, "method": "efectivo", "status": "pending" } ], "has_more": false, "next_cursor": null } ``` Instrucción `LINK`: `value` se abre para pagar.: ```json noexec="ejemplo" { "data": [ { "amount": "1250.00", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_amount": null, "expires_at": "2026-11-05T18:00:00.000Z", "external_ref": "pedido-7731", "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "instruction": { "type": "LINK", "value": "https://empresa-ejemplo.com/pagar/4hQvN8sL" }, "metadata": {}, "method": "spei", "status": "pending" } ], "has_more": false, "next_cursor": null } ``` Instrucción `QR`: `value` se muestra o se copia.: ```json noexec="ejemplo" { "data": [ { "amount": "1250.00", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_amount": null, "expires_at": "2026-11-05T18:00:00.000Z", "external_ref": "pedido-7731", "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "instruction": { "type": "QR", "value": "00020101021226580014ejemplo0212FICTICIO12345204000053034845802MX" }, "metadata": {}, "method": "spei", "status": "pending" } ], "has_more": false, "next_cursor": null } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "collections:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/listar-cobros/). #### Guías - [Cobros](/docs/recetas/cobros/) - [Paginación](/docs/paginacion/) - [Autenticación](/docs/autenticacion/) ### POST /v1/collections Crear un cobro Crea un cobro y devuelve la instrucción para quien paga. Exige `Idempotency-Key` y firma. Envía `amount` o `destination_amount`. 201 si quedó creado; 202 si aún no se confirma: repite con la misma clave. | Dato | Valor | |---|---| | Método y ruta | `POST /v1/collections` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `createCollection` | | Acceso | Llave con el scope `collections:write` | | Firma de petición | **Obligatoria** en producción y en la cuenta de prueba. El simulador público no la valida. Ver [Firma de petición](/docs/autenticacion/#firma-de-petición) | | Idempotencia | **Obligatoria**, con `Idempotency-Key`. Ver [Idempotencia](/docs/idempotencia/) | - Envía `amount`, lo que paga tu cliente, o `destination_amount`, lo que quieres recibir. Al menos uno. - `payer` lleva los datos de quien paga, y se comprueba contra `payer_fields` de [`GET /v1/collection-methods`](/docs/operaciones/listar-metodos-de-cobro/). `countrySymbol`, en 2 letras, sólo si quien paga es de otro país que la moneda: si no, se deduce de la moneda. - `202` significa que el alta aún no se confirma: `instruction` y `expires_at` llegan en `null`. Repite con la misma clave, o consulta el `id`. #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `Content-Type` | Sí | `application/json`. | | `Idempotency-Key` | Sí | Clave única de esta operación, de 8 a 200 caracteres. Repetirla con el mismo cuerpo devuelve la misma respuesta durante 7 días. Con otro cuerpo da `422 idempotency_key_reused`, también pasado ese plazo. | | `X-Fital-Timestamp` | Sí | Hora Unix en segundos. Se acepta hasta 300 s de diferencia. | | `X-Fital-Signature` | Sí | Firma `v1=` de la petición con el secreto de firma de la llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Cuerpo | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `amount` | texto | No | Monto que paga tu cliente. Envía éste o `destination_amount`. | | `currency` | texto | Sí | Moneda del cobro. | | `description` | texto | No | Concepto del cobro, hasta 140 caracteres imprimibles. Hasta 140 caracteres. | | `destination_amount` | texto | No | Monto que quieres recibir. Envía éste o `amount`. | | `expires_at` | texto | No | Vencimiento deseado. Tiene que ser futuro en un cobro `efectivo`, que sin este campo vence a los 3 días. ISO 8601 en UTC. | | `external_ref` | texto | No | Tu referencia del cobro. Vuelve en el cobro y en sus eventos. Hasta 120 caracteres. | | `metadata` | objeto | No | Hasta 20 pares clave y valor tuyos. | | `method` | texto | Sí | Método de `GET /v1/collection-methods`. `efectivo`, disponible bajo solicitud a FITAL, cobra en tiendas de México, en MXN: se paga con una referencia y se envía `amount`, no `destination_amount`. Hasta 40 caracteres. | | `payer` | objeto | No | Datos de quien paga. `payer_fields` de `GET /v1/collection-methods` dice qué pide cada método: sin un campo `required`, o con un valor fuera de `options`, la respuesta es `400 invalid_request` con el campo en `details.fields`, antes de crear nada. `countrySymbol`, en 2 letras, sólo si quien paga es de otro país que la moneda. | | `quote_id` | texto | No | Cotización previa del cobro, si la tienes. Hasta 100 caracteres. | ```json noexec="ejemplo" { "currency": "MXN", "method": "spei", "amount": "1250.00", "payer": { "firstName": "CLIENTE", "lastName": "FICTICIO", "email": "pagos@empresa-ejemplo.com" }, "description": "Pedido 7731", "external_ref": "pedido-7731" } ``` #### Respuestas correctas ##### 201 Creado | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `amount` | texto o `null` | Sí | Monto que paga tu cliente. | | `created_at` | texto o `null` | Sí | Fecha de creación. ISO 8601 en UTC. | | `currency` | texto o `null` | Sí | Moneda del cobro. | | `destination_amount` | texto o `null` | Sí | Monto que recibes. | | `expires_at` | texto o `null` | Sí | Vencimiento del cobro. ISO 8601 en UTC. | | `external_ref` | texto o `null` | Sí | Tu referencia. | | `id` | texto | Sí | Identificador del cobro, con prefijo `col_`. | | `instruction` | objeto o `null` | Sí | Cómo pagar. null mientras no esté lista. | | `instruction.type` | texto o `null` | Sí | Tipo de instrucción. Depende del método: trata la lista como extensible. `LINK`: `value` es una dirección que se abre para pagar. `QR`: `value` se muestra o se copia, como un QR o un texto de pago. `REFERENCE`: `value` es la referencia que quien paga da en la tienda, en un cobro `efectivo`. `clabe`: `value` es una CLABE. Sólo en sandbox. | | `instruction.value` | texto o `null` | Sí | Dato que usa quien paga. | | `metadata` | objeto | Sí | Los pares que enviaste al crear el cobro. | | `method` | texto o `null` | Sí | Método del cobro. | | `status` | texto | Sí | Estado del cobro. `processing`: El alta aún no se confirma. `pending`: Espera el pago. `paid`: Pagado. No vuelve atrás. En `efectivo`, la tienda ya confirmó el pago y el abono a tu cuenta llega después. `expired`: Venció sin pago. No vuelve atrás. `failed`: No se pudo crear o completar. | ###### Valores de `instruction.type` | Valor | Significado | |---|---| | `LINK` | `value` es una dirección que se abre para pagar. | | `QR` | `value` se muestra o se copia, como un QR o un texto de pago. | | `REFERENCE` | `value` es la referencia que quien paga da en la tienda, en un cobro `efectivo`. | | `clabe` | `value` es una CLABE. Sólo en la cuenta de prueba. | Depende del método. Trata la lista como extensible. ###### Valores de `status` | Valor | Significado | |---|---| | `processing` | El alta aún no se confirma. | | `pending` | Espera el pago. | | `paid` | Pagado. No vuelve atrás. | | `expired` | Venció sin pago. No vuelve atrás. | | `failed` | No se pudo crear o completar. | ```json noexec="ejemplo" { "amount": "1250.00", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_amount": null, "expires_at": "2026-11-05T18:00:00.000Z", "external_ref": "pedido-7731", "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "instruction": { "type": "clabe", "value": "999180000000000022" }, "metadata": {}, "method": "spei", "status": "pending" } ``` Instrucción `clabe`: sólo en sandbox.: ```json noexec="ejemplo" { "amount": "1250.00", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_amount": null, "expires_at": "2026-11-05T18:00:00.000Z", "external_ref": "pedido-7731", "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "instruction": { "type": "clabe", "value": "999180000000000022" }, "metadata": {}, "method": "spei", "status": "pending" } ``` Instrucción `REFERENCE`: quien paga da `value` en la tienda.: ```json noexec="ejemplo" { "amount": "1250.00", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_amount": null, "expires_at": "2026-11-05T18:00:00.000Z", "external_ref": "pedido-7731", "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "instruction": { "type": "REFERENCE", "value": "999000000000000000000000000022" }, "metadata": {}, "method": "efectivo", "status": "pending" } ``` Instrucción `LINK`: `value` se abre para pagar: ```json noexec="ejemplo" { "amount": "1250.00", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_amount": null, "expires_at": "2026-11-05T18:00:00.000Z", "external_ref": "pedido-7731", "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "instruction": { "type": "LINK", "value": "https://empresa-ejemplo.com/pagar/4hQvN8sL" }, "metadata": {}, "method": "spei", "status": "pending" } ``` Instrucción `QR`: `value` se muestra o se copia: ```json noexec="ejemplo" { "amount": "1250.00", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_amount": null, "expires_at": "2026-11-05T18:00:00.000Z", "external_ref": "pedido-7731", "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "instruction": { "type": "QR", "value": "00020101021226580014ejemplo0212FICTICIO12345204000053034845802MX" }, "metadata": {}, "method": "spei", "status": "pending" } ``` ##### 202 Aceptado: el resultado final llega después Misma forma que la respuesta anterior. ```json noexec="ejemplo" { "amount": "1250.00", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_amount": null, "expires_at": null, "external_ref": "pedido-7731", "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "instruction": null, "metadata": {}, "method": "spei", "status": "processing" } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Idempotent-Replayed` | `true` si la respuesta repite una anterior con esa clave. | | `Location` | Ruta del recurso, con `201` y `202`. | | `Retry-After` | Segundos a esperar, con `429`, con `409 idempotency_in_progress` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Falta `amount` o `destination_amount`, o algún campo no cumple el esquema. También sin un campo `required` de `payer_fields`, o con un valor fuera de `options`: `details.fields` lo nombra como `payer.`. | `false` | | 400 | [`idempotency_key_required`](/docs/errores/#idempotency_key_required) | Falta `Idempotency-Key`. | `false` | | 400 | [`idempotency_key_invalid`](/docs/errores/#idempotency_key_invalid) | La clave no mide entre 8 y 200 caracteres, o viene duplicada. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 401 | [`invalid_signature`](/docs/errores/#invalid_signature) | Falta la firma, no coincide o tiene más de 5 minutos. | `false` | | 403 | [`account_restricted`](/docs/errores/#account_restricted) | La cuenta tiene una restricción. | `false` | | 403 | [`not_enabled`](/docs/errores/#not_enabled) | El método no está activado para tu empresa. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 409 | [`collection_not_ready`](/docs/errores/#collection_not_ready) | El cobro aún no se puede crear porque falta una revisión de tu cuenta. | `false` | | 409 | [`idempotency_in_progress`](/docs/errores/#idempotency_in_progress) | Una petición anterior con esa clave sigue en curso. Llega con `Retry-After`. | `true` | | 413 | [`invalid_request`](/docs/errores/#invalid_request) | El cuerpo supera 64 KB. | `false` | | 422 | [`rail_unavailable`](/docs/errores/#rail_unavailable) | El país del método no está disponible por ahora. | `true` | | 422 | [`in_review`](/docs/errores/#in_review) | Una revisión de tu cuenta frena la operación. | `true` | | 422 | [`method_unavailable`](/docs/errores/#method_unavailable) | El método no está disponible. | `false` | | 422 | [`amount_out_of_range`](/docs/errores/#amount_out_of_range) | El monto está fuera del rango del método. | `false` | | 422 | [`collection_rejected`](/docs/errores/#collection_rejected) | El cobro se rechazó. | `false` | | 422 | [`idempotency_key_reused`](/docs/errores/#idempotency_key_reused) | Usaste esa clave con otro cuerpo, también pasados 7 días. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 429 | [`sandbox_limit`](/docs/errores/#sandbox_limit) (sólo cuenta de prueba) | Alcanzaste 100 pagos y cobros de hoy. | `false` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "amount": "Valor inválido" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 account_restricted`: ```json noexec="ejemplo" { "error": { "code": "account_restricted", "message": "Tu cuenta tiene una restricción. Contacta a tu ejecutivo FITAL.", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#account_restricted" } } ``` `409 collection_not_ready`: ```json noexec="ejemplo" { "error": { "code": "collection_not_ready", "message": "Operación rechazada", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#collection_not_ready" } } ``` `413 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `422 rail_unavailable`: ```json noexec="ejemplo" { "error": { "code": "rail_unavailable", "message": "Operación rechazada", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rail_unavailable" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Ante un timeout, repite con la misma clave. Un `400` por `payer` no crea nada, pero la clave conserva ese cuerpo: el cuerpo corregido necesita otra `Idempotency-Key`. Con la misma responde `422 idempotency_key_reused`. - `409 idempotency_in_progress`: Espera `Retry-After` y repite con la misma clave. - `422 rail_unavailable`: Repite más tarde. - `422 in_review`: Mira `retryable`. Con `true`, repite con la misma clave cuando la revisión se levante. Con `false`, el rechazo quedó registrado y esa clave lo repite: usa una clave nueva tras la revisión. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/crear-cobro/). #### Guías - [Cobros](/docs/recetas/cobros/) - [Cobertura](/docs/cobertura/) - [Autenticación](/docs/autenticacion/) - [Idempotencia](/docs/idempotencia/) - [Lista de migración](/docs/migracion/) ### POST /v1/collections/estimate Estimar un cobro Estima montos y comisiones de un cobro antes de crearlo. Es indicativa y no crea nada, por eso no pide `Idempotency-Key`. Va firmada como las demás escrituras de cobros. | Dato | Valor | |---|---| | Método y ruta | `POST /v1/collections/estimate` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `estimateCollection` | | Acceso | Llave con el scope `collections:write` | | Firma de petición | **Obligatoria** en producción y en la cuenta de prueba. El simulador público no la valida. Ver [Firma de petición](/docs/autenticacion/#firma-de-petición) | | Idempotencia | No aplica | - Es indicativa y no crea nada: por eso no pide `Idempotency-Key`. Firma con la última línea vacía. Ver [Firma de petición](/docs/autenticacion/#firma-de-petición). #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `Content-Type` | Sí | `application/json`. | | `X-Fital-Timestamp` | Sí | Hora Unix en segundos. Se acepta hasta 300 s de diferencia. | | `X-Fital-Signature` | Sí | Firma `v1=` de la petición con el secreto de firma de la llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Cuerpo | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `amount` | texto | Sí | Monto a estimar. | | `currency` | texto | Sí | Moneda del cobro. | | `method` | texto | Sí | Método de cobro. Hasta 40 caracteres. | ```json noexec="ejemplo" { "currency": "MXN", "method": "spei", "amount": "1250.00" } ``` #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `amount` | texto o `null` | Sí | Monto estimado. | | `currency` | texto o `null` | Sí | Moneda del monto. | | `exchange_rate` | texto o `null` | Sí | Tipo de cambio aplicado, si hay conversión. | | `expires_at` | texto o `null` | Sí | Vigencia de la estimación. ISO 8601 en UTC. | | `fee` | texto o `null` | Sí | Comisión. | | `fee_currency` | texto o `null` | Sí | Moneda de la comisión. | | `indicative` | booleano | Sí | Siempre true: es una estimación. | | `max_amount` | texto o `null` | Sí | Monto máximo del método. | | `min_amount` | texto o `null` | Sí | Monto mínimo del método. | | `plan_fee_pct` | texto o `null` | Sí | Comisión de tu plan, en porcentaje. | | `receive_amount` | texto o `null` | Sí | Lo que recibirías. null en `efectivo`: se fija al conciliar el pago. | | `receive_currency` | texto o `null` | Sí | Moneda de lo que recibirías. | | `total_cost_pct` | texto o `null` | Sí | Costo total, en porcentaje. | ```json noexec="ejemplo" { "amount": "1250.00", "currency": "MXN", "exchange_rate": null, "expires_at": "2026-11-04T18:05:00.000Z", "fee": "25.00", "fee_currency": "MXN", "indicative": true, "max_amount": "50000.00", "min_amount": "10.00", "plan_fee_pct": "2.00", "receive_amount": "1225.00", "receive_currency": "MXN", "total_cost_pct": "2.00" } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 401 | [`invalid_signature`](/docs/errores/#invalid_signature) | Falta la firma, no coincide o tiene más de 5 minutos. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 413 | [`invalid_request`](/docs/errores/#invalid_request) | El cuerpo supera 64 KB. | `false` | | 422 | [`method_unavailable`](/docs/errores/#method_unavailable) | El método no está disponible. | `false` | | 422 | [`amount_out_of_range`](/docs/errores/#amount_out_of_range) | El monto está fuera del rango del método. | `false` | | 422 | [`collection_rejected`](/docs/errores/#collection_rejected) | La estimación se rechazó. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "amount": "Valor inválido" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "collections:write" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `413 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `422 method_unavailable`: ```json noexec="ejemplo" { "error": { "code": "method_unavailable", "message": "Operación rechazada", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#method_unavailable" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos No lleva clave de idempotencia y no mueve dinero: repetirla es seguro. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/estimar-cobro/). #### Guías - [Cobros](/docs/recetas/cobros/) - [Autenticación](/docs/autenticacion/) ### GET /v1/collections/{id} Un cobro Un cobro de tu empresa, con su estado actual. `paid` y `expired` no vuelven atrás. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/collections/{id}` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `getCollection` | | Acceso | Llave con el scope `collections:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Parámetros | Parámetro | En | Tipo | Obligatorio | Descripción | |---|---|---|---|---| | `id` | ruta | texto | Sí | Identificador del recurso. Hasta 250 caracteres. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `amount` | texto o `null` | Sí | Monto que paga tu cliente. | | `created_at` | texto o `null` | Sí | Fecha de creación. ISO 8601 en UTC. | | `currency` | texto o `null` | Sí | Moneda del cobro. | | `destination_amount` | texto o `null` | Sí | Monto que recibes. | | `expires_at` | texto o `null` | Sí | Vencimiento del cobro. ISO 8601 en UTC. | | `external_ref` | texto o `null` | Sí | Tu referencia. | | `id` | texto | Sí | Identificador del cobro, con prefijo `col_`. | | `instruction` | objeto o `null` | Sí | Cómo pagar. null mientras no esté lista. | | `instruction.type` | texto o `null` | Sí | Tipo de instrucción. Depende del método: trata la lista como extensible. `LINK`: `value` es una dirección que se abre para pagar. `QR`: `value` se muestra o se copia, como un QR o un texto de pago. `REFERENCE`: `value` es la referencia que quien paga da en la tienda, en un cobro `efectivo`. `clabe`: `value` es una CLABE. Sólo en sandbox. | | `instruction.value` | texto o `null` | Sí | Dato que usa quien paga. | | `metadata` | objeto | Sí | Los pares que enviaste al crear el cobro. | | `method` | texto o `null` | Sí | Método del cobro. | | `status` | texto | Sí | Estado del cobro. `processing`: El alta aún no se confirma. `pending`: Espera el pago. `paid`: Pagado. No vuelve atrás. En `efectivo`, la tienda ya confirmó el pago y el abono a tu cuenta llega después. `expired`: Venció sin pago. No vuelve atrás. `failed`: No se pudo crear o completar. | ###### Valores de `instruction.type` | Valor | Significado | |---|---| | `LINK` | `value` es una dirección que se abre para pagar. | | `QR` | `value` se muestra o se copia, como un QR o un texto de pago. | | `REFERENCE` | `value` es la referencia que quien paga da en la tienda, en un cobro `efectivo`. | | `clabe` | `value` es una CLABE. Sólo en la cuenta de prueba. | Depende del método. Trata la lista como extensible. ###### Valores de `status` | Valor | Significado | |---|---| | `processing` | El alta aún no se confirma. | | `pending` | Espera el pago. | | `paid` | Pagado. No vuelve atrás. | | `expired` | Venció sin pago. No vuelve atrás. | | `failed` | No se pudo crear o completar. | ```json noexec="ejemplo" { "amount": "1250.00", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_amount": null, "expires_at": "2026-11-05T18:00:00.000Z", "external_ref": "pedido-7731", "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "instruction": { "type": "clabe", "value": "999180000000000022" }, "metadata": {}, "method": "spei", "status": "pending" } ``` Instrucción `clabe`: sólo en sandbox.: ```json noexec="ejemplo" { "amount": "1250.00", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_amount": null, "expires_at": "2026-11-05T18:00:00.000Z", "external_ref": "pedido-7731", "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "instruction": { "type": "clabe", "value": "999180000000000022" }, "metadata": {}, "method": "spei", "status": "pending" } ``` Instrucción `REFERENCE`: quien paga da `value` en la tienda.: ```json noexec="ejemplo" { "amount": "1250.00", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_amount": null, "expires_at": "2026-11-05T18:00:00.000Z", "external_ref": "pedido-7731", "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "instruction": { "type": "REFERENCE", "value": "999000000000000000000000000022" }, "metadata": {}, "method": "efectivo", "status": "pending" } ``` Instrucción `LINK`: `value` se abre para pagar: ```json noexec="ejemplo" { "amount": "1250.00", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_amount": null, "expires_at": "2026-11-05T18:00:00.000Z", "external_ref": "pedido-7731", "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "instruction": { "type": "LINK", "value": "https://empresa-ejemplo.com/pagar/4hQvN8sL" }, "metadata": {}, "method": "spei", "status": "pending" } ``` Instrucción `QR`: `value` se muestra o se copia: ```json noexec="ejemplo" { "amount": "1250.00", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_amount": null, "expires_at": "2026-11-05T18:00:00.000Z", "external_ref": "pedido-7731", "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "instruction": { "type": "QR", "value": "00020101021226580014ejemplo0212FICTICIO12345204000053034845802MX" }, "metadata": {}, "method": "spei", "status": "pending" } ``` Pagado: ```json noexec="ejemplo" { "amount": "1250.00", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_amount": null, "expires_at": "2026-11-05T18:00:00.000Z", "external_ref": "pedido-7731", "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "instruction": { "type": "clabe", "value": "999180000000000022" }, "metadata": {}, "method": "spei", "status": "paid" } ``` Venció sin pago: ```json noexec="ejemplo" { "amount": "1250.00", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_amount": null, "expires_at": "2026-11-05T18:00:00.000Z", "external_ref": "pedido-7731", "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "instruction": { "type": "clabe", "value": "999180000000000022" }, "metadata": {}, "method": "spei", "status": "expired" } ``` Alta sin confirmar: ```json noexec="ejemplo" { "amount": "1250.00", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_amount": null, "expires_at": null, "external_ref": "pedido-7731", "id": "col_4hQvN8sLd1ZxK6tB2mYpR7cW", "instruction": null, "metadata": {}, "method": "spei", "status": "processing" } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 404 | [`not_found`](/docs/errores/#not_found) | El cobro no existe o es de otra empresa. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "collections:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `404 not_found`: ```json noexec="ejemplo" { "error": { "code": "not_found", "message": "No encontrado", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#not_found" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/consultar-cobro/). #### Guías - [Cobros](/docs/recetas/cobros/) - [Ambientes](/docs/ambientes/) - [Autenticación](/docs/autenticacion/) - [Lista de migración](/docs/migracion/) ## Depósitos ### GET /v1/deposit-addresses Direcciones de depósito Direcciones de depósito de la empresa. Paginación por cursor. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/deposit-addresses` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `listDepositAddresses` | | Acceso | Llave con el scope `deposit_addresses:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Parámetros | Parámetro | En | Tipo | Obligatorio | Descripción | |---|---|---|---|---| | `limit` | consulta | texto | No | Filas por página, de 1 a 100. Por omisión, 25. | | `cursor` | consulta | texto | No | Valor de `next_cursor` de la página anterior. Hasta 1000 caracteres. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `data` | lista de objeto | Sí | Filas de esta página. | | `data[].address` | texto o `null` | Sí | Dirección donde depositar. null mientras esté en `processing`. | | `data[].chain` | texto o `null` | Sí | Red de la dirección, en minúsculas. | | `data[].created_at` | texto o `null` | Sí | Fecha de creación. ISO 8601 en UTC. | | `data[].currency` | texto o `null` | Sí | Token que acepta, en mayúsculas. | | `data[].id` | texto | Sí | Identificador de la dirección, con prefijo `dep_`. | | `data[].status` | texto | Sí | Estado de la dirección. `processing`: El alta aún no se confirma y los demás campos llegan en null. `active`: Ya recibe depósitos. | | `data[].warning` | texto o `null` | Sí | Advertencia de uso. Muéstrala a quien vaya a depositar: dice también si en esa red un depósito rechazado no regresa automáticamente. | | `has_more` | booleano | Sí | true si hay más filas después de esta página. | | `next_cursor` | texto o `null` | Sí | Cursor de la página siguiente. null si no hay. | ###### Valores de `data[].status` | Valor | Significado | |---|---| | `processing` | El alta aún no se confirma y los demás campos llegan en `null`. | | `active` | Ya recibe depósitos. | ```json noexec="ejemplo" { "data": [ { "address": "0x0000000000000000000000000000000000000000", "chain": "polygon", "created_at": "2026-11-04T18:00:00.000Z", "currency": "USDC", "id": "dep_9vLsK3mNc7QxB1tZ5hYdR2fP", "status": "active", "warning": "Envía sólo USDC por la red polygon. Otro token u otra red puede no acreditarse." } ], "has_more": false, "next_cursor": null } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "deposit_addresses:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/listar-direcciones-de-deposito/). #### Guías - [Direcciones de depósito](/docs/recetas/direcciones-deposito/) - [Paginación](/docs/paginacion/) - [Autenticación](/docs/autenticacion/) ### POST /v1/deposit-addresses Crear una dirección de depósito Crea una dirección de depósito para una red y un token. Exige `Idempotency-Key` y firma. 201 si quedó activa; 202 si aún no se confirma: repite con la misma clave. | Dato | Valor | |---|---| | Método y ruta | `POST /v1/deposit-addresses` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `createDepositAddress` | | Acceso | Llave con el scope `deposit_addresses:write` | | Firma de petición | **Obligatoria** en producción y en la cuenta de prueba. El simulador público no la valida. Ver [Firma de petición](/docs/autenticacion/#firma-de-petición) | | Idempotencia | **Obligatoria**, con `Idempotency-Key`. Ver [Idempotencia](/docs/idempotencia/) | - `202` significa que el alta aún no se confirma: `address` llega en `null`. No la compartas. Repite con la misma clave, o consulta el `id`. #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `Content-Type` | Sí | `application/json`. | | `Idempotency-Key` | Sí | Clave única de esta operación, de 8 a 200 caracteres. Repetirla con el mismo cuerpo devuelve la misma respuesta durante 7 días. Con otro cuerpo da `422 idempotency_key_reused`, también pasado ese plazo. | | `X-Fital-Timestamp` | Sí | Hora Unix en segundos. Se acepta hasta 300 s de diferencia. | | `X-Fital-Signature` | Sí | Firma `v1=` de la petición con el secreto de firma de la llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Cuerpo | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `chain` | texto | Sí | Red de la dirección, por ejemplo `polygon`. No distingue mayúsculas. Hasta 30 caracteres. | | `currency` | texto | Sí | Token que recibirá, por ejemplo `USDC`. No distingue mayúsculas. Hasta 10 caracteres. | ```json noexec="ejemplo" { "chain": "polygon", "currency": "USDC" } ``` #### Respuestas correctas ##### 201 Creado | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `address` | texto o `null` | Sí | Dirección donde depositar. null mientras esté en `processing`. | | `chain` | texto o `null` | Sí | Red de la dirección, en minúsculas. | | `created_at` | texto o `null` | Sí | Fecha de creación. ISO 8601 en UTC. | | `currency` | texto o `null` | Sí | Token que acepta, en mayúsculas. | | `id` | texto | Sí | Identificador de la dirección, con prefijo `dep_`. | | `status` | texto | Sí | Estado de la dirección. `processing`: El alta aún no se confirma y los demás campos llegan en null. `active`: Ya recibe depósitos. | | `warning` | texto o `null` | Sí | Advertencia de uso. Muéstrala a quien vaya a depositar: dice también si en esa red un depósito rechazado no regresa automáticamente. | ###### Valores de `status` | Valor | Significado | |---|---| | `processing` | El alta aún no se confirma y los demás campos llegan en `null`. | | `active` | Ya recibe depósitos. | ```json noexec="ejemplo" { "address": "0x0000000000000000000000000000000000000000", "chain": "polygon", "created_at": "2026-11-04T18:00:00.000Z", "currency": "USDC", "id": "dep_9vLsK3mNc7QxB1tZ5hYdR2fP", "status": "active", "warning": "Envía sólo USDC por la red polygon. Otro token u otra red puede no acreditarse." } ``` ##### 202 Aceptado: el resultado final llega después Misma forma que la respuesta anterior. ```json noexec="ejemplo" { "address": null, "chain": null, "created_at": null, "currency": null, "id": "dep_9vLsK3mNc7QxB1tZ5hYdR2fP", "status": "processing", "warning": null } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Idempotent-Replayed` | `true` si la respuesta repite una anterior con esa clave. | | `Location` | Ruta del recurso, con `201` y `202`. | | `Retry-After` | Segundos a esperar, con `429`, con `409 idempotency_in_progress` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 400 | [`idempotency_key_required`](/docs/errores/#idempotency_key_required) | Falta `Idempotency-Key`. | `false` | | 400 | [`idempotency_key_invalid`](/docs/errores/#idempotency_key_invalid) | La clave no mide entre 8 y 200 caracteres, o viene duplicada. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 401 | [`invalid_signature`](/docs/errores/#invalid_signature) | Falta la firma, no coincide o tiene más de 5 minutos. | `false` | | 403 | [`account_restricted`](/docs/errores/#account_restricted) | La cuenta tiene una restricción. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 409 | [`idempotency_in_progress`](/docs/errores/#idempotency_in_progress) | Una petición anterior con esa clave sigue en curso. Llega con `Retry-After`. | `true` | | 413 | [`invalid_request`](/docs/errores/#invalid_request) | El cuerpo supera 64 KB. | `false` | | 422 | [`in_review`](/docs/errores/#in_review) | Una revisión de tu cuenta frena la operación. | `true` | | 422 | [`pair_not_supported`](/docs/errores/#pair_not_supported) | Esa combinación de `chain` y `currency` no está disponible. Ver [Cobertura](/docs/cobertura/#redes-y-tokens). | `false` | | 422 | [`deposit_address_rejected`](/docs/errores/#deposit_address_rejected) | La dirección se rechazó. | `false` | | 422 | [`idempotency_key_reused`](/docs/errores/#idempotency_key_reused) | Usaste esa clave con otro cuerpo, también pasados 7 días. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "chain": "Valor inválido" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 account_restricted`: ```json noexec="ejemplo" { "error": { "code": "account_restricted", "message": "Tu cuenta tiene una restricción. Contacta a tu ejecutivo FITAL.", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#account_restricted" } } ``` `409 idempotency_in_progress`: ```json noexec="ejemplo" { "error": { "code": "idempotency_in_progress", "message": "Operación en curso", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#idempotency_in_progress" } } ``` `413 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `422 in_review`: ```json noexec="ejemplo" { "error": { "code": "in_review", "message": "La operación está en revisión", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#in_review" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Ante un timeout o una conexión cortada, repite con la misma clave. - `409 idempotency_in_progress`: Espera `Retry-After` y repite con la misma clave. - `422 in_review`: Mira `retryable`. Con `true`, repite con la misma clave cuando la revisión se levante. Con `false`, el rechazo quedó registrado y esa clave lo repite: usa una clave nueva tras la revisión. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/crear-direccion-de-deposito/). #### Guías - [Direcciones de depósito](/docs/recetas/direcciones-deposito/) - [Cobertura](/docs/cobertura/) - [Autenticación](/docs/autenticacion/) - [Lista de migración](/docs/migracion/) ### GET /v1/deposit-addresses/{id} Una dirección de depósito Una dirección de depósito de tu empresa. Si el alta quedó sin confirmar responde `processing`, con los datos en null. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/deposit-addresses/{id}` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `getDepositAddress` | | Acceso | Llave con el scope `deposit_addresses:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Parámetros | Parámetro | En | Tipo | Obligatorio | Descripción | |---|---|---|---|---| | `id` | ruta | texto | Sí | Identificador del recurso. Hasta 250 caracteres. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `address` | texto o `null` | Sí | Dirección donde depositar. null mientras esté en `processing`. | | `chain` | texto o `null` | Sí | Red de la dirección, en minúsculas. | | `created_at` | texto o `null` | Sí | Fecha de creación. ISO 8601 en UTC. | | `currency` | texto o `null` | Sí | Token que acepta, en mayúsculas. | | `id` | texto | Sí | Identificador de la dirección, con prefijo `dep_`. | | `status` | texto | Sí | Estado de la dirección. `processing`: El alta aún no se confirma y los demás campos llegan en null. `active`: Ya recibe depósitos. | | `warning` | texto o `null` | Sí | Advertencia de uso. Muéstrala a quien vaya a depositar: dice también si en esa red un depósito rechazado no regresa automáticamente. | ###### Valores de `status` | Valor | Significado | |---|---| | `processing` | El alta aún no se confirma y los demás campos llegan en `null`. | | `active` | Ya recibe depósitos. | ```json noexec="ejemplo" { "address": "0x0000000000000000000000000000000000000000", "chain": "polygon", "created_at": "2026-11-04T18:00:00.000Z", "currency": "USDC", "id": "dep_9vLsK3mNc7QxB1tZ5hYdR2fP", "status": "active", "warning": "Envía sólo USDC por la red polygon. Otro token u otra red puede no acreditarse." } ``` Alta sin confirmar: ```json noexec="ejemplo" { "address": null, "chain": null, "created_at": null, "currency": null, "id": "dep_9vLsK3mNc7QxB1tZ5hYdR2fP", "status": "processing", "warning": null } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 404 | [`not_found`](/docs/errores/#not_found) | La dirección no existe o es de otra empresa. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "deposit_addresses:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `404 not_found`: ```json noexec="ejemplo" { "error": { "code": "not_found", "message": "No encontrado", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#not_found" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/consultar-direccion-de-deposito/). #### Guías - [Direcciones de depósito](/docs/recetas/direcciones-deposito/) - [Autenticación](/docs/autenticacion/) ### GET /v1/deposits Depósitos recibidos y quién pagó Depósitos recibidos y quién pagó, del más nuevo al más viejo. `payer` llega en null si no hay datos legibles. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/deposits` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `listDeposits` | | Acceso | Llave con el scope `deposits:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Parámetros | Parámetro | En | Tipo | Obligatorio | Descripción | |---|---|---|---|---| | `limit` | consulta | texto | No | Filas por página, de 1 a 100. Por omisión, 25. | | `cursor` | consulta | texto | No | Valor de `next_cursor` de la página anterior. Hasta 1000 caracteres. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `data` | lista de objeto | Sí | Filas de esta página. | | `data[].amount` | texto o `null` | Sí | Monto recibido. | | `data[].concept` | texto o `null` | Sí | Concepto que escribió quien pagó. | | `data[].credited_amount` | texto o `null` | Sí | Monto acreditado a tu saldo. | | `data[].credited_currency` | texto o `null` | Sí | Moneda acreditada. | | `data[].currency` | texto o `null` | Sí | Moneda recibida. | | `data[].id` | texto | Sí | Identificador del depósito, con prefijo `dpt_`. | | `data[].payer` | objeto o `null` | Sí | Quién pagó. null si no hay datos legibles: nunca se inventa. | | `data[].payer.address` | texto o `null` | Sí | Dirección de origen. null si no se conoce. | | `data[].payer.bank` | texto o `null` | Sí | Banco de origen. null si no se conoce. | | `data[].payer.clabe` | texto o `null` | Sí | CLABE de origen. null si no se conoce. | | `data[].payer.name` | texto o `null` | Sí | Nombre de quien pagó. null si no se conoce. | | `data[].payer.rfc` | texto o `null` | Sí | RFC de quien pagó. null si no se conoce. | | `data[].receipt_url` | texto o `null` | Sí | Enlace https al comprobante, si existe. | | `data[].received_at` | texto o `null` | Sí | Fecha del depósito. ISO 8601 en UTC. | | `data[].reference` | texto o `null` | Sí | Referencia del depósito. | | `data[].status` | texto | Sí | Estado del depósito. `in_review`: El depósito está en revisión. Aún no es dinero disponible. `received`: El dinero ya está acreditado. `returned`: El depósito se devolvió. | | `data[].tracking_key` | texto o `null` | Sí | Clave de rastreo del banco. | | `has_more` | booleano | Sí | true si hay más filas después de esta página. | | `next_cursor` | texto o `null` | Sí | Cursor de la página siguiente. null si no hay. | ###### Valores de `data[].status` | Valor | Significado | |---|---| | `in_review` | El depósito está en revisión. Aún no es dinero disponible. | | `received` | El dinero ya está acreditado. | | `returned` | El depósito se devolvió. | ```json noexec="ejemplo" { "data": [ { "amount": "1250.00", "concept": "Pedido 7731", "credited_amount": "1250.00", "credited_currency": "MXN", "currency": "MXN", "id": "dpt_7bTcF2qXe9WmL4sD1nHrY6vK", "payer": { "address": null, "bank": "Banco de ejemplo", "clabe": "999180000000000022", "name": "CLIENTE FICTICIO SA DE CV", "rfc": "XAXX010101000" }, "receipt_url": null, "received_at": "2026-11-04T18:00:00.000Z", "reference": "7731", "status": "received", "tracking_key": "CLAVE-DE-RASTREO-DEMO" } ], "has_more": false, "next_cursor": null } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "deposits:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/listar-depositos/). #### Guías - [Quién pagó](/docs/recetas/quien-pago/) - [Conciliar](/docs/recetas/conciliar/) - [Paginación](/docs/paginacion/) - [Ambientes](/docs/ambientes/) - [Autenticación](/docs/autenticacion/) ### GET /v1/deposits/{id} Un depósito Un depósito de tu empresa. El id va sellado por empresa: el de otra da 404. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/deposits/{id}` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `getDeposit` | | Acceso | Llave con el scope `deposits:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Parámetros | Parámetro | En | Tipo | Obligatorio | Descripción | |---|---|---|---|---| | `id` | ruta | texto | Sí | Identificador del recurso. Hasta 250 caracteres. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `amount` | texto o `null` | Sí | Monto recibido. | | `concept` | texto o `null` | Sí | Concepto que escribió quien pagó. | | `credited_amount` | texto o `null` | Sí | Monto acreditado a tu saldo. | | `credited_currency` | texto o `null` | Sí | Moneda acreditada. | | `currency` | texto o `null` | Sí | Moneda recibida. | | `id` | texto | Sí | Identificador del depósito, con prefijo `dpt_`. | | `payer` | objeto o `null` | Sí | Quién pagó. null si no hay datos legibles: nunca se inventa. | | `payer.address` | texto o `null` | Sí | Dirección de origen. null si no se conoce. | | `payer.bank` | texto o `null` | Sí | Banco de origen. null si no se conoce. | | `payer.clabe` | texto o `null` | Sí | CLABE de origen. null si no se conoce. | | `payer.name` | texto o `null` | Sí | Nombre de quien pagó. null si no se conoce. | | `payer.rfc` | texto o `null` | Sí | RFC de quien pagó. null si no se conoce. | | `receipt_url` | texto o `null` | Sí | Enlace https al comprobante, si existe. | | `received_at` | texto o `null` | Sí | Fecha del depósito. ISO 8601 en UTC. | | `reference` | texto o `null` | Sí | Referencia del depósito. | | `status` | texto | Sí | Estado del depósito. `in_review`: El depósito está en revisión. Aún no es dinero disponible. `received`: El dinero ya está acreditado. `returned`: El depósito se devolvió. | | `tracking_key` | texto o `null` | Sí | Clave de rastreo del banco. | ###### Valores de `status` | Valor | Significado | |---|---| | `in_review` | El depósito está en revisión. Aún no es dinero disponible. | | `received` | El dinero ya está acreditado. | | `returned` | El depósito se devolvió. | ```json noexec="ejemplo" { "amount": "1250.00", "concept": "Pedido 7731", "credited_amount": "1250.00", "credited_currency": "MXN", "currency": "MXN", "id": "dpt_7bTcF2qXe9WmL4sD1nHrY6vK", "payer": { "address": null, "bank": "Banco de ejemplo", "clabe": "999180000000000022", "name": "CLIENTE FICTICIO SA DE CV", "rfc": "XAXX010101000" }, "receipt_url": null, "received_at": "2026-11-04T18:00:00.000Z", "reference": "7731", "status": "received", "tracking_key": "CLAVE-DE-RASTREO-DEMO" } ``` En revisión: aún no es dinero disponible: ```json noexec="ejemplo" { "amount": "1250.00", "concept": "Pedido 7731", "credited_amount": null, "credited_currency": null, "currency": "MXN", "id": "dpt_7bTcF2qXe9WmL4sD1nHrY6vK", "payer": { "address": null, "bank": "Banco de ejemplo", "clabe": "999180000000000022", "name": "CLIENTE FICTICIO SA DE CV", "rfc": "XAXX010101000" }, "receipt_url": null, "received_at": "2026-11-04T18:00:00.000Z", "reference": "7731", "status": "in_review", "tracking_key": "CLAVE-DE-RASTREO-DEMO" } ``` Sin datos legibles del pagador: ```json noexec="ejemplo" { "amount": "1250.00", "concept": "Pedido 7731", "credited_amount": "1250.00", "credited_currency": "MXN", "currency": "MXN", "id": "dpt_7bTcF2qXe9WmL4sD1nHrY6vK", "payer": null, "receipt_url": null, "received_at": "2026-11-04T18:00:00.000Z", "reference": "7731", "status": "received", "tracking_key": "CLAVE-DE-RASTREO-DEMO" } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 404 | [`not_found`](/docs/errores/#not_found) | El depósito no existe o es de otra empresa. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "deposits:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `404 not_found`: ```json noexec="ejemplo" { "error": { "code": "not_found", "message": "No encontrado", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#not_found" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/consultar-deposito/). #### Guías - [Quién pagó](/docs/recetas/quien-pago/) - [Autenticación](/docs/autenticacion/) ## Movimientos ### GET /v1/movements Movimientos de la cuenta Movimientos de la cuenta, de esta API y de otros orígenes, en el orden en que cambiaron. `payment_id` y `external_ref` dicen de qué pago sale cada uno. Al llegar al final `has_more` es `false` y `next_cursor` sigue presente: guárdalo y úsalo después para recibir sólo lo nuevo. Ese recorrido siguiente empieza en la hora en que abrió el anterior y repasa 2 minutos más, para no perder lo que confirmó tarde, y una fila que cambia mientras recorres vuelve a salir. Por eso un mismo id puede repetirse: quédate con la copia de `updated_at` más reciente. Con `from` y `to`, `from` debe ser anterior. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/movements` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `listMovements` | | Acceso | Llave con el scope `movements:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | - Una fila que cambia mientras recorres puede repetirse, y nunca se omite otra. Deduplica por `id`. `next_cursor` puede venir con valor aunque `has_more` sea `false`: guárdalo. Ver [Paginación](/docs/paginacion/#movimientos-un-cursor-que-no-termina). #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Parámetros | Parámetro | En | Tipo | Obligatorio | Descripción | |---|---|---|---|---| | `limit` | consulta | texto | No | Filas por página, de 1 a 100. Por omisión, 25. | | `cursor` | consulta | texto | No | Valor de `next_cursor` de la página anterior. Hasta 1000 caracteres. | | `from` | consulta | texto | No | Desde esta fecha de creación, incluida. ISO 8601 en UTC. | | `to` | consulta | texto | No | Hasta esta fecha de creación, sin incluirla. ISO 8601 en UTC. | | `type` | consulta | texto | No | Filtra por categoría. `payment`: Salida por un riel de pago. `deposit`: Entrada de dinero. `crypto`: Envío o recepción de un token. `internal`: Movimiento interno de la cuenta. `card`: Operación con tarjeta. `other`: Cualquier otro movimiento. | | `currency` | consulta | texto | No | Filtra por cualquiera de las monedas del movimiento. | ##### Valores de `type` | Valor | Significado | |---|---| | `payment` | Salida por un riel de pago. | | `deposit` | Entrada de dinero. | | `crypto` | Envío o recepción de un token. | | `internal` | Movimiento interno de la cuenta. | | `card` | Operación con tarjeta. | | `other` | Cualquier otro movimiento. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `data` | lista de objeto | Sí | Filas de esta página. | | `data[].amount` | texto o `null` | Sí | Monto del movimiento. | | `data[].completed_at` | texto o `null` | Sí | Fecha en que terminó. ISO 8601 en UTC. | | `data[].created_at` | texto o `null` | Sí | Fecha de creación. ISO 8601 en UTC. | | `data[].currency` | texto o `null` | Sí | Moneda del monto. | | `data[].destination_currency` | texto o `null` | Sí | Moneda de destino. | | `data[].external_ref` | texto o `null` | Sí | Tu `external_ref` del pago o cobro del que sale este movimiento. null si no tiene. | | `data[].final_amount` | texto o `null` | Sí | Monto final, cuando se conoce. | | `data[].final_currency` | texto o `null` | Sí | Moneda del monto final. | | `data[].id` | texto | Sí | Identificador del movimiento, con prefijo `mov_`. | | `data[].payment_id` | texto o `null` | Sí | Pago de esta API del que sale este movimiento, con prefijo `pay_`. null si no salió de un pago creado por esta API. | | `data[].read_status` | texto | Sí | Cuánto se pudo leer. `complete`: Se leyó el detalle completo. `partial`: No se pudo leer el detalle completo. El estado no avanza con una lectura parcial. | | `data[].status` | texto | Sí | Estado público. Sólo `completed`, `failed`, `returned` y `canceled` son terminales. `processing`: Aceptado y en curso, o aún sin confirmar. Consúltalo hasta un estado terminal. `in_review`: En revisión. No lo repitas: el pago ya existe. `completed`: Se pagó. Aún puede pasar a `returned`. `failed`: No se hizo y no queda dinero en tránsito. `returned`: Se devolvió y el saldo ya volvió. `canceled`: Se canceló antes de ejecutarse. | | `data[].type` | texto | Sí | Categoría del movimiento. `payment`: Salida por un riel de pago. `deposit`: Entrada de dinero. `crypto`: Envío o recepción de un token. `internal`: Movimiento interno de la cuenta. `card`: Operación con tarjeta. `other`: Cualquier otro movimiento. | | `data[].updated_at` | texto o `null` | Sí | Última actualización. ISO 8601 en UTC. | | `has_more` | booleano | Sí | true si hay más filas después de esta página. | | `next_cursor` | texto o `null` | Sí | Cursor de la página siguiente. null si no hay. | ###### Valores de `data[].read_status` | Valor | Significado | |---|---| | `complete` | Se leyó el detalle completo. | | `partial` | No se pudo leer el detalle completo. El estado no avanza con una lectura parcial. | ###### Valores de `data[].status` | Valor | Significado | |---|---| | `processing` | Aceptado y en curso, o aún sin confirmar. Consúltalo hasta un estado terminal. | | `in_review` | En revisión. No lo repitas: el pago ya existe. | | `completed` | Se pagó. Aún puede pasar a `returned`. | | `failed` | No se hizo y no queda dinero en tránsito. | | `returned` | Se devolvió y el saldo ya volvió. | | `canceled` | Se canceló antes de ejecutarse. | ###### Valores de `data[].type` | Valor | Significado | |---|---| | `payment` | Salida por un riel de pago. | | `deposit` | Entrada de dinero. | | `crypto` | Envío o recepción de un token. | | `internal` | Movimiento interno de la cuenta. | | `card` | Operación con tarjeta. | | `other` | Cualquier otro movimiento. | ```json noexec="ejemplo" { "data": [ { "amount": "1500.00", "completed_at": "2026-11-04T18:01:30.000Z", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_currency": "MXN", "external_ref": "pedido-7731", "final_amount": "1500.00", "final_currency": "MXN", "id": "mov_2hKsV9tBq4XzM7nD1cYpL6rF", "payment_id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "read_status": "complete", "status": "completed", "type": "payment", "updated_at": "2026-11-04T18:01:30.000Z" } ], "has_more": false, "next_cursor": null } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún filtro no cumple el esquema, `from` no es anterior a `to`, o el cursor es inválido o de otra consulta. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "movements:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/listar-movimientos/). #### Guías - [Movimientos y recibos](/docs/recetas/movimientos-y-recibos/) - [Conciliar](/docs/recetas/conciliar/) - [Paginación](/docs/paginacion/) - [Autenticación](/docs/autenticacion/) - [Lista de migración](/docs/migracion/) - [Montos y fechas](/docs/montos-y-fechas/) ### GET /v1/movements/{id} Un movimiento Un movimiento de tu empresa. El id va sellado por empresa: el de otra da 404. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/movements/{id}` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `getMovement` | | Acceso | Llave con el scope `movements:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Parámetros | Parámetro | En | Tipo | Obligatorio | Descripción | |---|---|---|---|---| | `id` | ruta | texto | Sí | Identificador del recurso. Hasta 250 caracteres. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `amount` | texto o `null` | Sí | Monto del movimiento. | | `completed_at` | texto o `null` | Sí | Fecha en que terminó. ISO 8601 en UTC. | | `created_at` | texto o `null` | Sí | Fecha de creación. ISO 8601 en UTC. | | `currency` | texto o `null` | Sí | Moneda del monto. | | `destination_currency` | texto o `null` | Sí | Moneda de destino. | | `external_ref` | texto o `null` | Sí | Tu `external_ref` del pago o cobro del que sale este movimiento. null si no tiene. | | `final_amount` | texto o `null` | Sí | Monto final, cuando se conoce. | | `final_currency` | texto o `null` | Sí | Moneda del monto final. | | `id` | texto | Sí | Identificador del movimiento, con prefijo `mov_`. | | `payment_id` | texto o `null` | Sí | Pago de esta API del que sale este movimiento, con prefijo `pay_`. null si no salió de un pago creado por esta API. | | `read_status` | texto | Sí | Cuánto se pudo leer. `complete`: Se leyó el detalle completo. `partial`: No se pudo leer el detalle completo. El estado no avanza con una lectura parcial. | | `status` | texto | Sí | Estado público. Sólo `completed`, `failed`, `returned` y `canceled` son terminales. `processing`: Aceptado y en curso, o aún sin confirmar. Consúltalo hasta un estado terminal. `in_review`: En revisión. No lo repitas: el pago ya existe. `completed`: Se pagó. Aún puede pasar a `returned`. `failed`: No se hizo y no queda dinero en tránsito. `returned`: Se devolvió y el saldo ya volvió. `canceled`: Se canceló antes de ejecutarse. | | `type` | texto | Sí | Categoría del movimiento. `payment`: Salida por un riel de pago. `deposit`: Entrada de dinero. `crypto`: Envío o recepción de un token. `internal`: Movimiento interno de la cuenta. `card`: Operación con tarjeta. `other`: Cualquier otro movimiento. | | `updated_at` | texto o `null` | Sí | Última actualización. ISO 8601 en UTC. | ###### Valores de `read_status` | Valor | Significado | |---|---| | `complete` | Se leyó el detalle completo. | | `partial` | No se pudo leer el detalle completo. El estado no avanza con una lectura parcial. | ###### Valores de `status` | Valor | Significado | |---|---| | `processing` | Aceptado y en curso, o aún sin confirmar. Consúltalo hasta un estado terminal. | | `in_review` | En revisión. No lo repitas: el pago ya existe. | | `completed` | Se pagó. Aún puede pasar a `returned`. | | `failed` | No se hizo y no queda dinero en tránsito. | | `returned` | Se devolvió y el saldo ya volvió. | | `canceled` | Se canceló antes de ejecutarse. | ###### Valores de `type` | Valor | Significado | |---|---| | `payment` | Salida por un riel de pago. | | `deposit` | Entrada de dinero. | | `crypto` | Envío o recepción de un token. | | `internal` | Movimiento interno de la cuenta. | | `card` | Operación con tarjeta. | | `other` | Cualquier otro movimiento. | ```json noexec="ejemplo" { "amount": "1500.00", "completed_at": "2026-11-04T18:01:30.000Z", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_currency": "MXN", "external_ref": "pedido-7731", "final_amount": "1500.00", "final_currency": "MXN", "id": "mov_2hKsV9tBq4XzM7nD1cYpL6rF", "payment_id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "read_status": "complete", "status": "completed", "type": "payment", "updated_at": "2026-11-04T18:01:30.000Z" } ``` Lectura parcial: el estado no avanza: ```json noexec="ejemplo" { "amount": "1500.00", "completed_at": null, "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "destination_currency": "MXN", "external_ref": "pedido-7731", "final_amount": null, "final_currency": null, "id": "mov_2hKsV9tBq4XzM7nD1cYpL6rF", "payment_id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "read_status": "partial", "status": "processing", "type": "payment", "updated_at": "2026-11-04T18:01:30.000Z" } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 404 | [`not_found`](/docs/errores/#not_found) | El movimiento no existe o es de otra empresa. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "movements:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `404 not_found`: ```json noexec="ejemplo" { "error": { "code": "not_found", "message": "No encontrado", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#not_found" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/consultar-movimiento/). #### Guías - [Movimientos y recibos](/docs/recetas/movimientos-y-recibos/) - [Autenticación](/docs/autenticacion/) ### GET /v1/movements/{id}/receipt Recibo de un movimiento terminal Recibo de un movimiento terminal. Con `Accept: application/json` responde datos; si no, un PDF. Antes de terminar da `409 receipt_not_available`. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/movements/{id}/receipt` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `getMovementReceipt` | | Acceso | Llave con el scope `movements:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | - Con `Accept: application/json` responde datos. Con cualquier otro valor responde `application/pdf`. - `completed_at` es `null` si el movimiento no terminó en `completed`: un recibo de `failed`, `returned` o `canceled` no trae fecha de completado. #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | | `Accept` | No | `application/json` para recibir datos. Sin ese valor, responde un PDF. | #### Parámetros | Parámetro | En | Tipo | Obligatorio | Descripción | |---|---|---|---|---| | `id` | ruta | texto | Sí | Identificador del recurso. Hasta 250 caracteres. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `amount` | texto o `null` | Sí | Monto del movimiento. | | `bank` | texto o `null` | Sí | Banco de destino. | | `beneficiary` | texto o `null` | Sí | Quién recibió. | | `completed_at` | texto o `null` | Sí | Fecha en que terminó. ISO 8601 en UTC. | | `concept` | texto o `null` | Sí | Concepto. | | `created_at` | texto o `null` | Sí | Fecha de creación. ISO 8601 en UTC. | | `currency` | texto o `null` | Sí | Moneda del monto. | | `external_ref` | texto o `null` | Sí | Tu `external_ref` del pago o cobro del que sale este recibo. null si no tiene. | | `final_amount` | texto o `null` | Sí | Monto final. | | `final_currency` | texto o `null` | Sí | Moneda del monto final. | | `id` | texto | Sí | Identificador del movimiento. | | `payment_id` | texto o `null` | Sí | Pago de esta API del que sale este recibo, con prefijo `pay_`. null si no salió de un pago creado por esta API. | | `status` | texto | Sí | Estado público. Sólo `completed`, `failed`, `returned` y `canceled` son terminales. `processing`: Aceptado y en curso, o aún sin confirmar. Consúltalo hasta un estado terminal. `in_review`: En revisión. No lo repitas: el pago ya existe. `completed`: Se pagó. Aún puede pasar a `returned`. `failed`: No se hizo y no queda dinero en tránsito. `returned`: Se devolvió y el saldo ya volvió. `canceled`: Se canceló antes de ejecutarse. | | `type` | texto | Sí | Categoría del movimiento. `payment`: Salida por un riel de pago. `deposit`: Entrada de dinero. `crypto`: Envío o recepción de un token. `internal`: Movimiento interno de la cuenta. `card`: Operación con tarjeta. `other`: Cualquier otro movimiento. | ###### Valores de `status` | Valor | Significado | |---|---| | `processing` | Aceptado y en curso, o aún sin confirmar. Consúltalo hasta un estado terminal. | | `in_review` | En revisión. No lo repitas: el pago ya existe. | | `completed` | Se pagó. Aún puede pasar a `returned`. | | `failed` | No se hizo y no queda dinero en tránsito. | | `returned` | Se devolvió y el saldo ya volvió. | | `canceled` | Se canceló antes de ejecutarse. | ###### Valores de `type` | Valor | Significado | |---|---| | `payment` | Salida por un riel de pago. | | `deposit` | Entrada de dinero. | | `crypto` | Envío o recepción de un token. | | `internal` | Movimiento interno de la cuenta. | | `card` | Operación con tarjeta. | | `other` | Cualquier otro movimiento. | ```json noexec="ejemplo" { "amount": "1500.00", "bank": "Banco de ejemplo", "beneficiary": "PROVEEDOR FICTICIO SA DE CV", "completed_at": "2026-11-04T18:01:30.000Z", "concept": "Factura 1042", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "external_ref": "pedido-7731", "final_amount": "1500.00", "final_currency": "MXN", "id": "mov_2hKsV9tBq4XzM7nD1cYpL6rF", "payment_id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "status": "completed", "type": "payment" } ``` `failed`: `completed_at` en `null`: ```json noexec="ejemplo" { "amount": "1500.00", "bank": "Banco de ejemplo", "beneficiary": "PROVEEDOR FICTICIO SA DE CV", "completed_at": null, "concept": "Factura 1042", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "external_ref": "pedido-7731", "final_amount": null, "final_currency": null, "id": "mov_2hKsV9tBq4XzM7nD1cYpL6rF", "payment_id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "status": "failed", "type": "payment" } ``` Sin `Accept: application/json`, responde estos mismos datos como `application/pdf`. #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 404 | [`not_found`](/docs/errores/#not_found) | El movimiento no existe o es de otra empresa. | `false` | | 409 | [`receipt_not_available`](/docs/errores/#receipt_not_available) | El movimiento aún no es terminal, o no tiene recibo. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "movements:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `404 not_found`: ```json noexec="ejemplo" { "error": { "code": "not_found", "message": "No encontrado", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#not_found" } } ``` `409 receipt_not_available`: ```json noexec="ejemplo" { "error": { "code": "receipt_not_available", "message": "El movimiento aún no tiene recibo", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#receipt_not_available" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/descargar-recibo/). #### Guías - [Movimientos y recibos](/docs/recetas/movimientos-y-recibos/) - [Autenticación](/docs/autenticacion/) ## Eventos ### GET /v1/events Eventos de la empresa Eventos de la empresa, del más nuevo al más viejo. Sirve para recuperar lo que un webhook no entregó: pide sin cursor y recorre hasta un evento que ya procesaste. `next_cursor` sólo camina hacia el pasado y termina en null, así que no lo guardes entre consultas. Filtra con `type`. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/events` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `listEvents` | | Acceso | Llave con el scope `events:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | - Va del más nuevo al más viejo, así que `next_cursor` sólo camina hacia el pasado y termina en `null`. Para ver lo nuevo, pide la lista sin `cursor` y recorre hasta un evento que ya procesaste. Ver [El evento siempre queda guardado](/docs/webhooks/#el-evento-siempre-queda-guardado). #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Parámetros | Parámetro | En | Tipo | Obligatorio | Descripción | |---|---|---|---|---| | `limit` | consulta | texto | No | Filas por página, de 1 a 100. Por omisión, 25. | | `cursor` | consulta | texto | No | Valor de `next_cursor` de la página anterior. Hasta 1000 caracteres. | | `type` | consulta | texto | No | Filtra por tipo de evento, por ejemplo `payment.completed`. Hasta 60 caracteres. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `data` | lista de objeto | Sí | Filas de esta página. | | `data[].api_version` | texto | Sí | Versión con que se generó el evento. | | `data[].created_at` | texto o `null` | Sí | Fecha de creación. ISO 8601 en UTC. | | `data[].data` | objeto | Sí | Contenido del evento. Su forma depende de `type`. | | `data[].id` | texto | Sí | Identificador del evento, con prefijo `evt_`. Va firmado: úsalo para ignorar repetidos. | | `data[].resource_id` | texto | Sí | Recurso al que se refiere. | | `data[].sequence` | entero | Sí | Orden dentro del recurso: un valor mayor es más reciente. | | `data[].type` | texto | Sí | Tipo de evento. Ignora los que no conozcas. `payment.processing`: El pago se aceptó y sigue en curso, o se reabrió. `payment.in_review`: El pago está en revisión. No lo repitas. `payment.completed`: El pago se confirmó. `payment.failed`: El pago no se hizo y no queda dinero en tránsito. `payment.returned`: El pago se devolvió. Puede llegar después de `payment.completed`. `payment.canceled`: El pago se canceló. `deposit.received`: El dinero ya está acreditado. `deposit.in_review`: El depósito está en revisión. `deposit.returned`: El depósito se devolvió. `collection.paid`: El cobro se pagó. En `efectivo`, la tienda ya confirmó el pago y el abono a tu cuenta llega después. `collection.expired`: El cobro venció sin pago. `security.ip_allowlist.changed`: Cambió la lista de IP de una llave de producción. `system.egress_ips.changing`: Cambiará una IP de salida de webhooks. Actualiza tu firewall antes de `effective_at`. | | `has_more` | booleano | Sí | true si hay más filas después de esta página. | | `next_cursor` | texto o `null` | Sí | Cursor de la página siguiente. null si no hay. | ###### Valores de `data[].data` Cambia según `type`. Ver el [catálogo de eventos](/docs/webhooks/eventos/). ###### Valores de `data[].type` Ver el [catálogo de eventos](/docs/webhooks/eventos/). Cada tipo está en el contrato, y aun así ignora los que no conozcas: un tipo nuevo no rompe la versión. ```json noexec="ejemplo" { "data": [ { "api_version": "2026-11-01", "created_at": "2026-11-04T18:01:30.000Z", "data": { "amount": "1500.00", "currency": "MXN", "destination_currency": "MXN", "external_ref": "pedido-7731", "failure_code": null, "final_amount": "1500.00", "final_currency": "MXN", "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "status": "completed", "updated_at": "2026-11-04T18:01:30.000Z" }, "id": "evt_5mPqW2xZr8TnC1vH7kLdS4bJ", "resource_id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "sequence": 2, "type": "payment.completed" } ], "has_more": false, "next_cursor": null } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "events:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/listar-eventos/). #### Guías - [Webhooks](/docs/webhooks/) - [Catálogo de eventos](/docs/webhooks/eventos/) - [Paginación](/docs/paginacion/) - [Autenticación](/docs/autenticacion/) - [Lista de migración](/docs/migracion/) - [Conciliar](/docs/recetas/conciliar/) - [Salud y estado](/docs/salud-y-estado/) - [Cuenta de prueba](/docs/sandbox/cuenta-de-prueba/) - [IP de webhooks](/docs/seguridad/ip-webhooks/) ### GET /v1/events/{id} Un evento Un evento de tu empresa, con el mismo contenido que entrega el webhook. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/events/{id}` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `getEvent` | | Acceso | Llave con el scope `events:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Parámetros | Parámetro | En | Tipo | Obligatorio | Descripción | |---|---|---|---|---| | `id` | ruta | texto | Sí | Identificador del recurso. Hasta 250 caracteres. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `api_version` | texto | Sí | Versión con que se generó el evento. | | `created_at` | texto o `null` | Sí | Fecha de creación. ISO 8601 en UTC. | | `data` | objeto | Sí | Contenido del evento. Su forma depende de `type`. | | `id` | texto | Sí | Identificador del evento, con prefijo `evt_`. Va firmado: úsalo para ignorar repetidos. | | `resource_id` | texto | Sí | Recurso al que se refiere. | | `sequence` | entero | Sí | Orden dentro del recurso: un valor mayor es más reciente. | | `type` | texto | Sí | Tipo de evento. Ignora los que no conozcas. `payment.processing`: El pago se aceptó y sigue en curso, o se reabrió. `payment.in_review`: El pago está en revisión. No lo repitas. `payment.completed`: El pago se confirmó. `payment.failed`: El pago no se hizo y no queda dinero en tránsito. `payment.returned`: El pago se devolvió. Puede llegar después de `payment.completed`. `payment.canceled`: El pago se canceló. `deposit.received`: El dinero ya está acreditado. `deposit.in_review`: El depósito está en revisión. `deposit.returned`: El depósito se devolvió. `collection.paid`: El cobro se pagó. En `efectivo`, la tienda ya confirmó el pago y el abono a tu cuenta llega después. `collection.expired`: El cobro venció sin pago. `security.ip_allowlist.changed`: Cambió la lista de IP de una llave de producción. `system.egress_ips.changing`: Cambiará una IP de salida de webhooks. Actualiza tu firewall antes de `effective_at`. | ###### Valores de `data` Cambia según `type`. Ver el [catálogo de eventos](/docs/webhooks/eventos/). ###### Valores de `type` Ver el [catálogo de eventos](/docs/webhooks/eventos/). Cada tipo está en el contrato, y aun así ignora los que no conozcas: un tipo nuevo no rompe la versión. ```json noexec="ejemplo" { "api_version": "2026-11-01", "created_at": "2026-11-04T18:01:30.000Z", "data": { "amount": "1500.00", "currency": "MXN", "destination_currency": "MXN", "external_ref": "pedido-7731", "failure_code": null, "final_amount": "1500.00", "final_currency": "MXN", "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "status": "completed", "updated_at": "2026-11-04T18:01:30.000Z" }, "id": "evt_5mPqW2xZr8TnC1vH7kLdS4bJ", "resource_id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "sequence": 2, "type": "payment.completed" } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 404 | [`not_found`](/docs/errores/#not_found) | El evento no existe o es de otra empresa. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "events:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `404 not_found`: ```json noexec="ejemplo" { "error": { "code": "not_found", "message": "No encontrado", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#not_found" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/consultar-evento/). #### Guías - [Catálogo de eventos](/docs/webhooks/eventos/) - [Autenticación](/docs/autenticacion/) ## Webhooks ### GET /v1/webhook-endpoints Destinos de webhooks Destinos de webhooks de la empresa, sólo para consultarlos. El alta, la edición, la baja, la rotación del secreto y la reactivación se hacen en el panel: v1 no tiene API para eso. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/webhook-endpoints` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `listWebhookEndpoints` | | Acceso | Llave con el scope `webhooks:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | - Sólo consulta. El alta, la edición, la baja, la rotación del secreto y la reactivación de un destino se hacen en el panel, también en `v1`: un destino decide a dónde sale tu información, y el panel exige sesión, rol y bitácora. Ver [Dar de alta un destino](/docs/webhooks/#dar-de-alta-un-destino). #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `data` | lista de objeto | Sí | Destinos de webhooks de la empresa. | | `data[].created_at` | texto o `null` | Sí | Fecha de creación. ISO 8601 en UTC. | | `data[].events` | lista de texto | Sí | Tipos suscritos. Vacío: todos. | | `data[].id` | texto | Sí | Identificador del destino. | | `data[].status` | texto | Sí | Estado del destino. `active`: Recibe eventos. `disabled`: Dejó de recibir eventos tras 3 días seguidos de fallos. Se reactiva en el panel. `paused`: Valor reservado. Hoy ningún flujo lo asigna. | | `data[].url` | texto | Sí | URL https que recibe los eventos. | ###### Valores de `data[].status` | Valor | Significado | |---|---| | `active` | Recibe eventos. | | `disabled` | Dejó de recibir eventos tras 3 días seguidos de fallos. Se reactiva en el panel. | | `paused` | Valor reservado. Hoy ningún flujo lo asigna. | ```json noexec="ejemplo" { "data": [ { "created_at": "2026-11-04T18:00:00.000Z", "events": [ "payment.completed", "payment.failed" ], "id": "3f0c2d1e-7a54-4b1f-9c0e-1a2b3c4d5e6f", "status": "active", "url": "https://empresa-ejemplo.com/webhooks/fital" } ] } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) (sólo cuenta de prueba) | El encabezado `X-Fital-Scenario` trae un escenario desconocido. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "webhooks:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/listar-destinos-de-webhook/). #### Guías - [Webhooks](/docs/webhooks/) - [Inicio rápido de webhooks](/docs/webhooks/inicio-rapido/) - [Autenticación](/docs/autenticacion/) - [Paginación](/docs/paginacion/) ## Sistema ### GET /v1/health Salud pública del servicio Estado general del servicio, sin llave y sin gastar cuota. Sale de una medición en segundo plano: si tiene más de 15 s responde `degraded`, aunque dijera `ok`. Con `down` el código es 503. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/health` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `getHealth` | | Acceso | Público, sin llave | | Firma de petición | No aplica | | Idempotencia | No aplica | - Esta operación no usa la forma de [Errores](/docs/errores/): con `down` responde `503` con el mismo cuerpo que un `200`. Decide por `status`. #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| Sin llave ni otros encabezados obligatorios. #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `api_version` | texto | Sí | Versión de la API. `v1`: la única publicada. | | `status` | texto | Sí | Estado general del servicio. `ok`: El servicio funciona. `degraded`: La medición es vieja o hay problemas parciales. Responde `200`. `down`: El servicio no funciona. Responde `503`. | ###### Valores de `api_version` | Valor | Significado | |---|---| | `v1` | Versión de la API. | ###### Valores de `status` | Valor | Significado | |---|---| | `ok` | El servicio funciona. | | `degraded` | La medición tiene más de 15 segundos, o hay problemas parciales. Responde `200`. | | `down` | El servicio no funciona. Responde `503`. | ```json noexec="ejemplo" { "status": "ok", "api_version": "v1" } ``` Medición vieja o problema parcial: sigue en `200`: ```json noexec="ejemplo" { "status": "degraded", "api_version": "v1" } ``` ##### 503 Servicio no disponible Misma forma que la respuesta anterior. ```json noexec="ejemplo" { "status": "down", "api_version": "v1" } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | #### Reintentos Con `503`, repite con espera exponencial. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/consultar-salud/). #### Guías - [Salud y estado](/docs/salud-y-estado/) - [Autenticación](/docs/autenticacion/) - [Cuenta de prueba](/docs/sandbox/cuenta-de-prueba/) ### GET /v1/meta/egress-ips IP de salida de webhooks Direcciones desde las que FITAL envía webhooks, para tu firewall. Pública, con un minuto de caché. La firma HMAC sigue siendo la protección principal. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/meta/egress-ips` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `getEgressIps` | | Acceso | Público, sin llave | | Firma de petición | No aplica | | Idempotencia | No aplica | #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| Sin llave ni otros encabezados obligatorios. #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `addresses` | lista de objeto | Sí | Direcciones vigentes o anunciadas. | | `addresses[].active_from` | texto o `null` | Sí | Desde cuándo se usa. | | `addresses[].ip` | texto | Sí | Dirección IP de salida. | | `addresses[].retiring_at` | texto o `null` | Sí | Cuándo deja de usarse. null si no tiene fecha de retiro. | | `note` | texto | Sí | Recordatorio: autoriza por firma, no sólo por IP. | ```json noexec="ejemplo" { "addresses": [ { "active_from": "2026-11-01T00:00:00.000Z", "ip": "198.51.100.10", "retiring_at": null } ], "note": "La firma HMAC es la protección principal" } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste el límite por IP. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. | `true` | ##### Ejemplos de error `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/consultar-ip-de-salida/). #### Guías - [IP de webhooks](/docs/seguridad/ip-webhooks/) - [Autenticación](/docs/autenticacion/) - [Cuenta de prueba](/docs/sandbox/cuenta-de-prueba/) ### GET /v1/status Estado por capacidad Estado de pagos, cobros, webhooks y API para tu empresa. Acepta cualquier llave válida. `not_enabled` indica que tu empresa no tiene esa capacidad y `unknown` que no hay medición reciente. | Dato | Valor | |---|---| | Método y ruta | `GET /v1/status` | | Servidor | `https://widget.fital.xyz` | | `operationId` | `getStatus` | | Acceso | Cualquier llave válida | | Firma de petición | No aplica | | Idempotencia | No aplica | #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `capabilities` | lista de objeto | Sí | Una fila por capacidad. | | `capabilities[].id` | texto | Sí | Identificador de la capacidad. `payments_mxn`: Pagos por SPEI. `payments_latam`: Pagos por el riel `latam`. `payments_pix`: Pagos por PIX. `collections`: Cobros. `webhooks`: Entrega de webhooks. `api`: La API. | | `capabilities[].name` | texto | Sí | Nombre legible. | | `capabilities[].since` | texto o `null` | Sí | Desde cuándo está en ese estado. null si no se sabe. | | `capabilities[].status` | texto | Sí | Estado de la capacidad. `operational`: Funciona con normalidad. `degraded`: Funciona con problemas. `outage`: No funciona. `unknown`: Sin medición reciente. `not_enabled`: No está habilitada para tu empresa. | | `updated_at` | texto | Sí | Momento en que se armó esta respuesta. | ###### Valores de `capabilities[].id` | Valor | Significado | |---|---| | `payments_mxn` | Pagos por SPEI. | | `payments_latam` | Pagos por el riel `latam`. | | `payments_pix` | Pagos por PIX. | | `collections` | Cobros. | | `webhooks` | Entrega de webhooks. | | `api` | La API. | El contrato lo declara como texto: trata la lista como extensible. ###### Valores de `capabilities[].status` | Valor | Significado | |---|---| | `operational` | Funciona con normalidad. | | `degraded` | Funciona con problemas. | | `outage` | No funciona. | | `unknown` | Sin medición reciente. | | `not_enabled` | No está habilitada para tu empresa. | ```json noexec="ejemplo" { "capabilities": [ { "id": "payments_mxn", "name": "Pagos MXN", "since": "2026-11-04T10:00:00.000Z", "status": "operational" }, { "id": "payments_pix", "name": "Pagos PIX", "since": null, "status": "not_enabled" } ], "updated_at": "2026-11-04T18:00:00.000Z" } ``` Variante `status_degraded`: ```json noexec="ejemplo" { "updated_at": "2026-11-04T18:00:00.000Z", "capabilities": [ { "id": "payments_mxn", "name": "Pagos MXN", "status": "degraded", "since": "2026-11-04T10:00:00.000Z" }, { "id": "webhooks", "name": "Webhooks", "status": "operational", "since": "2026-11-04T09:00:00.000Z" }, { "id": "api", "name": "API", "status": "operational", "since": "2026-11-04T09:00:00.000Z" } ] } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) (sólo cuenta de prueba) | El encabezado `X-Fital-Scenario` trae un escenario desconocido. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 403 | [`kyb_required`](/docs/errores/#kyb_required) | La empresa no tiene KYB aprobado o aún no está inscrita. | `false` | | 403 | [`account_not_active`](/docs/errores/#account_not_active) | La cuenta no está activa. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "limit": "Entre 1 y 100" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 ip_not_allowed`: ```json noexec="ejemplo" { "error": { "code": "ip_not_allowed", "message": "IP no permitida para esta llave", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#ip_not_allowed" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Es una lectura: repetirla no tiene efecto. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/consultar-estado/). #### Guías - [Salud y estado](/docs/salud-y-estado/) - [Autenticación](/docs/autenticacion/) - [Cobertura](/docs/cobertura/) - [Pago PIX](/docs/recetas/pago-pix/) ## Sandbox ### POST /sandbox/v1/payments/{id}/advance Avanzar un pago ficticio Lleva un pago de sandbox a otro estado sin esperar su guion, y emite el evento firmado de ese cambio. Exige `Idempotency-Key` y firma. Sólo existe en sandbox: en producción da 404. | Dato | Valor | |---|---| | Método y ruta | `POST /sandbox/v1/payments/{id}/advance` | | Servidor | `https://widget.fital.xyz/sandbox` | | `operationId` | `advanceSandboxPayment` | | Acceso | Llave con el scope `payments:write` | | Firma de petición | **Obligatoria** en producción y en la cuenta de prueba. El simulador público no la valida. Ver [Firma de petición](/docs/autenticacion/#firma-de-petición) | | Idempotencia | **Obligatoria**, con `Idempotency-Key`. Ver [Idempotencia](/docs/idempotencia/) | | Ambiente | Sólo existe en la cuenta de prueba. En producción responde `404` | #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `Content-Type` | Sí | `application/json`. | | `Idempotency-Key` | Sí | Clave única de esta operación, de 8 a 200 caracteres. Repetirla con el mismo cuerpo devuelve la misma respuesta durante 7 días. Con otro cuerpo da `422 idempotency_key_reused`, también pasado ese plazo. | | `X-Fital-Timestamp` | Sí | Hora Unix en segundos. Se acepta hasta 300 s de diferencia. | | `X-Fital-Signature` | Sí | Firma `v1=` de la petición con el secreto de firma de la llave. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Parámetros | Parámetro | En | Tipo | Obligatorio | Descripción | |---|---|---|---|---| | `id` | ruta | texto | Sí | Identificador del recurso. Hasta 250 caracteres. | #### Cuerpo | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `status` | texto | Sí | Estado al que quieres llevar el pago ficticio. `completed`: Termina el pago. Viene de `processing` o `in_review`. `failed`: Falla el pago. Viene de `processing` o `in_review`. `returned`: Devuelve el pago. Viene de `completed`. `in_review`: Lo pasa a revisión. Viene de `processing`. | ```json noexec="ejemplo" { "status": "completed" } ``` ##### Valores de `status` | Valor | Significado | |---|---| | `completed` | Termina el pago. Viene de `processing` o `in_review`. | | `failed` | Falla el pago. Viene de `processing` o `in_review`. | | `returned` | Devuelve el pago. Viene de `completed`. | | `in_review` | Lo pasa a revisión. Viene de `processing`. | #### Respuestas correctas ##### 200 Correcto | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `amount` | texto o `null` | Sí | Monto pedido. | | `beneficiary_id` | texto o `null` | Sí | Beneficiario del pago. | | `created_at` | texto o `null` | Sí | Fecha de creación. ISO 8601 en UTC. | | `currency` | texto o `null` | Sí | Moneda pedida. | | `external_ref` | texto o `null` | Sí | Tu referencia. | | `failure_code` | texto o `null` | Sí | Código del catálogo de errores si el pago falló, null en cualquier otro estado. En `GET /v1/payments` siempre es null: léelo en `GET /v1/payments/{id}` o en `payment.failed`. Trata la lista como extensible. `payment_rejected`: Rechazado sin un motivo más específico. `insufficient_funds`: Saldo insuficiente al ejecutar. `limit_exceeded`: Se superó el tope acumulado de la empresa o un límite del riel. `amount_below_minimum`: El monto quedó por debajo del mínimo aplicable. `account_restricted`: La cuenta tiene una restricción. `in_review`: Una revisión detuvo la ejecución. `quote_changed`: El débito superaría lo permitido. `quote_expired`: La cotización venció antes de ejecutar. | | `final_amount` | texto o `null` | Sí | Monto final, cuando se conoce. | | `final_currency` | texto o `null` | Sí | Moneda del monto final. | | `freshness` | objeto | No | De dónde sale `status`. Sólo en `GET /v1/payments/{id}`. | | `freshness.as_of` | texto o `null` | Sí | Hora de esa lectura, de la última que se aplicó o de la cancelación confirmada. Null si aún no hay ninguna. ISO 8601 en UTC. | | `freshness.status` | texto | Sí | De dónde sale `status`. `current`: `status` sale de una lectura de ahora. `delayed`: La lectura no concluyó y `status` es lo último que se sabe. No lo tomes como un resultado nuevo. | | `id` | texto | Sí | Identificador del pago, con prefijo `pay_`. | | `metadata` | objeto | Sí | Los pares que enviaste al crear el pago. | | `reference` | texto o `null` | Sí | Concepto del pago. | | `status` | texto | Sí | Estado público. `processing`: Aceptado y en curso, o aún sin confirmar. Consúltalo hasta un estado terminal. `in_review`: En revisión. No lo repitas: el pago ya existe. `completed`: Se pagó. Aún puede pasar a `returned`. `failed`: No se hizo y no queda dinero en tránsito. `returned`: Se devolvió y el saldo ya volvió. `canceled`: Se canceló antes de ejecutarse. | ###### Valores de `failure_code` | Valor | Significado | |---|---| | `payment_rejected` | Rechazado sin un motivo más específico. | | `insufficient_funds` | Saldo insuficiente al ejecutar. | | `limit_exceeded` | Se superó el tope acumulado de la empresa o un límite del riel. | | `amount_below_minimum` | El monto quedó por debajo del mínimo aplicable. | | `account_restricted` | La cuenta tiene una restricción. | | `in_review` | Una revisión detuvo la ejecución. | | `quote_changed` | El débito superaría lo permitido. | | `quote_expired` | La cotización venció antes de ejecutar. | Con `failed`, `GET /v1/payments/{id}` y `payment.failed` siempre traen un código: `payment_rejected` si no hay uno más específico. En cualquier otro estado, y en la lista `GET /v1/payments`, es `null`. Trata la lista como extensible. ###### Valores de `freshness.status` | Valor | Significado | |---|---| | `current` | `status` sale de una lectura de ahora. | | `delayed` | La lectura no concluyó y `status` es lo último que se sabe. No lo tomes como un resultado nuevo. | ###### Valores de `status` | Valor | Significado | |---|---| | `processing` | Aceptado y en curso, o aún sin confirmar. Consúltalo hasta un estado terminal. | | `in_review` | En revisión. No lo repitas: el pago ya existe. | | `completed` | Se pagó. Aún puede pasar a `returned`. | | `failed` | No se hizo y no queda dinero en tránsito. | | `returned` | Se devolvió y el saldo ya volvió. | | `canceled` | Se canceló antes de ejecutarse. | ```json noexec="ejemplo" { "amount": "1500.00", "beneficiary_id": "ben_3kTMd9wQp2LxZ7vR1nYcA4bE", "created_at": "2026-11-04T18:00:00.000Z", "currency": "MXN", "external_ref": "pedido-7731", "failure_code": null, "final_amount": "1500.00", "final_currency": "MXN", "id": "pay_6nRtY1wKs3QzD8cL5mVhF2gB", "livemode": false, "metadata": {}, "reference": null, "status": "completed" } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Idempotent-Replayed` | `true` si la respuesta repite una anterior con esa clave. | | `Retry-After` | Segundos a esperar, con `429`, con `409 idempotency_in_progress` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 400 | [`idempotency_key_required`](/docs/errores/#idempotency_key_required) | Falta `Idempotency-Key`. | `false` | | 400 | [`idempotency_key_invalid`](/docs/errores/#idempotency_key_invalid) | La clave no mide entre 8 y 200 caracteres, o viene duplicada. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 401 | [`invalid_signature`](/docs/errores/#invalid_signature) | Falta la firma, no coincide o tiene más de 5 minutos. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 404 | [`not_found`](/docs/errores/#not_found) | El pago no existe o es de otra empresa. | `false` | | 409 | [`sandbox_invalid_transition`](/docs/errores/#sandbox_invalid_transition) | Un pago real no puede hacer ese cambio. | `false` | | 409 | [`idempotency_in_progress`](/docs/errores/#idempotency_in_progress) | Una petición anterior con esa clave sigue en curso. Llega con `Retry-After`. | `true` | | 413 | [`invalid_request`](/docs/errores/#invalid_request) | El cuerpo supera 64 KB. | `false` | | 422 | [`insufficient_funds`](/docs/errores/#insufficient_funds) | Llevar a `completed` un pago aún sin débito requiere saldo ficticio. | `false` | | 422 | [`idempotency_key_reused`](/docs/errores/#idempotency_key_reused) | Usaste esa clave con otro cuerpo, también pasados 7 días. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "status": "Valor inválido" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "payments:write" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `404 not_found`: ```json noexec="ejemplo" { "error": { "code": "not_found", "message": "No encontrado", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#not_found" } } ``` `409 sandbox_invalid_transition`: ```json noexec="ejemplo" { "error": { "code": "sandbox_invalid_transition", "message": "Operación rechazada", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#sandbox_invalid_transition" } } ``` `413 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `422 insufficient_funds`: ```json noexec="ejemplo" { "error": { "code": "insufficient_funds", "message": "Operación rechazada", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#insufficient_funds" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos Ante un timeout o una conexión cortada, repite con la misma clave. - `409 idempotency_in_progress`: Espera `Retry-After` y repite con la misma clave. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/avanzar-pago-de-prueba/). #### Guías - [Inicio rápido de la cuenta de prueba](/docs/sandbox/inicio-rapido/) - [Cuenta de prueba](/docs/sandbox/cuenta-de-prueba/) ### POST /sandbox/v1/webhooks/trigger Emitir un evento de prueba Emite un evento ficticio del tipo que pidas hacia tus destinos de sandbox, con firma y entrega reales. Sirve para probar tu receptor sin crear operaciones. Sólo existe en sandbox: en producción da 404. | Dato | Valor | |---|---| | Método y ruta | `POST /sandbox/v1/webhooks/trigger` | | Servidor | `https://widget.fital.xyz/sandbox` | | `operationId` | `triggerSandboxWebhook` | | Acceso | Llave con el scope `webhooks:read` | | Firma de petición | No aplica | | Idempotencia | No aplica | | Ambiente | Sólo existe en la cuenta de prueba. En producción responde `404` | - Sin clave de idempotencia y sin firma de petición: cada llamada emite otro evento. Necesita el scope `webhooks:read`. - `data` tiene la forma real de ese tipo de evento, con datos ficticios, más `simulated: true` y `livemode: false`. Ver el [catálogo de eventos](/docs/webhooks/eventos/). #### Encabezados de la petición | Encabezado | Obligatorio | Descripción | |---|---|---| | `Authorization` | Sí | `Bearer` y tu llave. | | `Content-Type` | Sí | `application/json`. | | `X-Fital-Scenario` | No | Sólo simuladores: elige la respuesta que quieres practicar. | #### Cuerpo | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `resource_id` | texto | No | Recurso al que se refiere. Sin este campo se usa uno ficticio. | | `type` | texto | Sí | Tipo de evento a emitir. `payment.processing`: El pago se aceptó y sigue en curso, o se reabrió. `payment.in_review`: El pago está en revisión. No lo repitas. `payment.completed`: El pago se confirmó. `payment.failed`: El pago no se hizo y no queda dinero en tránsito. `payment.returned`: El pago se devolvió. Puede llegar después de `payment.completed`. `payment.canceled`: El pago se canceló. `deposit.received`: El dinero ya está acreditado. `deposit.in_review`: El depósito está en revisión. `deposit.returned`: El depósito se devolvió. `collection.paid`: El cobro se pagó. En `efectivo`, la tienda ya confirmó el pago y el abono a tu cuenta llega después. `collection.expired`: El cobro venció sin pago. `security.ip_allowlist.changed`: Cambió la lista de IP de una llave de producción. `system.egress_ips.changing`: Cambiará una IP de salida de webhooks. Actualiza tu firewall antes de `effective_at`. | ```json noexec="ejemplo" { "type": "payment.completed" } ``` ##### Valores de `type` Ver el [catálogo de eventos](/docs/webhooks/eventos/). #### Respuestas correctas ##### 202 Aceptado: el resultado final llega después | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `event_id` | texto | Sí | Identificador del evento emitido. | | `resource_id` | texto | Sí | Recurso del evento. | | `sequence` | entero | Sí | Orden dentro del recurso. | ```json noexec="ejemplo" { "event_id": "evt_5mPqW2xZr8TnC1vH7kLdS4bJ", "livemode": false, "resource_id": "sim_7f3a9c21d4e8b605", "sequence": 1 } ``` #### Encabezados de la respuesta | Encabezado | Contenido | |---|---| | `X-Request-Id` | Identificador de la petición. Cítalo con soporte. | | `Retry-After` | Segundos a esperar, con `429` y a veces con `503`. | #### Errores Todos usan la forma de [Errores](/docs/errores/): `error.code`, `message`, `request_id`, `details`, `retryable` y `docs_url`. Decide por `code`. | HTTP | Código | Cuándo sale | `retryable` | |---|---|---|---| | 400 | [`invalid_request`](/docs/errores/#invalid_request) | Algún parámetro o campo no cumple el esquema. Mira `details.fields`. | `false` | | 401 | [`invalid_api_key`](/docs/errores/#invalid_api_key) | La llave falta, está mal formada, venció o fue revocada, o la empresa está suspendida. | `false` | | 401 | [`wrong_environment`](/docs/errores/#wrong_environment) | La llave es de otro ambiente. | `false` | | 403 | [`forbidden_scope`](/docs/errores/#forbidden_scope) | La llave no tiene el scope de esta operación. `details.required_scope` lo nombra. | `false` | | 403 | [`ip_not_allowed`](/docs/errores/#ip_not_allowed) | La IP de origen no está activa en la lista de esa llave. | `false` | | 413 | [`invalid_request`](/docs/errores/#invalid_request) | El cuerpo supera 64 KB. | `false` | | 429 | [`rate_limited`](/docs/errores/#rate_limited) | Superaste un límite de peticiones. Llega con `Retry-After`. | `true` | | 500 | [`internal_error`](/docs/errores/#internal_error) | Fallo nuestro. El resultado de la petición no se conoce. | `true` | | 503 | [`service_unavailable`](/docs/errores/#service_unavailable) | El servicio no está disponible por ahora. El resultado de la petición no se conoce. | `true` | ##### Ejemplos de error `400 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": { "fields": { "resource_id": "Valor inválido" } }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `401 invalid_api_key`: ```json noexec="ejemplo" { "error": { "code": "invalid_api_key", "message": "Credencial inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_api_key" } } ``` `403 forbidden_scope`: ```json noexec="ejemplo" { "error": { "code": "forbidden_scope", "message": "La llave no tiene permiso para esta operación", "request_id": "0123456789abcdef0123456789abcdef", "details": { "required_scope": "webhooks:read" }, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#forbidden_scope" } } ``` `413 invalid_request`: ```json noexec="ejemplo" { "error": { "code": "invalid_request", "message": "Petición inválida", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": false, "docs_url": "https://merchants.fital.xyz/docs/errores/#invalid_request" } } ``` `429 rate_limited`: ```json noexec="ejemplo" { "error": { "code": "rate_limited", "message": "Demasiadas peticiones", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#rate_limited" } } ``` `500 internal_error`: ```json noexec="ejemplo" { "error": { "code": "internal_error", "message": "Error interno", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#internal_error" } } ``` `503 service_unavailable`: ```json noexec="ejemplo" { "error": { "code": "service_unavailable", "message": "Servicio no disponible", "request_id": "0123456789abcdef0123456789abcdef", "details": {}, "retryable": true, "docs_url": "https://merchants.fital.xyz/docs/errores/#service_unavailable" } } ``` #### Reintentos No lleva clave de idempotencia y no mueve dinero: repetirla es seguro. - `429 rate_limited`: Espera lo que indique `Retry-After` y repite. - `500 internal_error`: Repite con espera exponencial. - `503 service_unavailable`: Repite con espera exponencial. Respeta `Retry-After` si viene. Ver [Reintentos](/docs/recetas/reintentos/) y [Límites](/docs/limites/). #### Muestras En curl, Node, Python y PHP: ver [la página de esta operación](/docs/operaciones/emitir-evento-de-prueba/). #### Guías - [Inicio rápido de webhooks](/docs/webhooks/inicio-rapido/) - [Cuenta de prueba](/docs/sandbox/cuenta-de-prueba/) - [Lista de migración](/docs/migracion/) - [Catálogo de eventos](/docs/webhooks/eventos/)