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

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
<?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";
```

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