# 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
<?php
$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 = (string) time();
$canon = implode("\n", [$ts, 'POST', '/v1/collections', hash('sha256', $cuerpo), $clave]);
$firma = hash_hmac('sha256', $canon, getenv('FITAL_SIGNING_SECRET'));
$c = curl_init(getenv('FITAL_API') . '/collections');
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";
```

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.<campo>`.
- 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`.
