# 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
<?php
$cuerpo = '{"rail":"mxn_spei","holder_name":"PROVEEDOR FICTICIO SA DE CV","clabe":"999180000000000011"}';
$clave = 'beneficiario-proveedor-77';
$ts = (string) time();
$canon = implode("\n", [$ts, 'POST', '/v1/beneficiaries', hash('sha256', $cuerpo), $clave]);
$firma = hash_hmac('sha256', $canon, getenv('FITAL_SIGNING_SECRET'));
$c = curl_init(getenv('FITAL_API') . '/beneficiaries');
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";
```

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
<?php
$cuerpo = '{"beneficiary_id":"ben_3kTMd9wQp2LxZ7vR1nYcA4bE","amount":"1500.00","currency":"MXN","external_ref":"factura-1042"}';
$clave = 'pago-factura-1042';
$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";
```

## 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
<?php
$c = curl_init(getenv('FITAL_API') . '/payments/pay_6nRtY1wKs3QzD8cL5mVhF2gB');
curl_setopt_array($c, [
  CURLOPT_RETURNTRANSFER => 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/).
