# Empezar La API v2 le permite **cobrarle a una persona** (pay-ins) y **pagarle a una persona** (payouts) con una sola integración: país, moneda y método viajan en el cuerpo del pedido, y lo que se puede hacer hoy lo dice `GET /v2/capabilities`. Usted no elige proveedor ni banco de salida: la API enruta cada operación por el mejor riel disponible. ## En cinco pasos 1. **Pida sus llaves.** TuCapi le entrega dos: `tuc_live_…` para producción (`https://api.tucapi.app`) y `tuc_test_…` para probar sin mover dinero en el sandbox (`https://api.tucapi.app/sandbox`), con los alcances que necesite (`payins:crear`, `payouts:crear`, `operaciones:leer`, `saldos:leer`, `webhooks:configurar`). Ver [Autenticación](autenticacion.md). 2. **Lea el catálogo.** `GET /v2/capabilities` le dice qué países, monedas y métodos tiene su llave, con los campos que cada método exige. Ver [Catálogo](catalogo.md). 3. **Registre su webhook.** `POST /v2/webhook-endpoints` con su url https; guarde el secreto, se muestra una sola vez. Ver [Webhooks](webhooks.md). 4. **Cree su primera operación** con un SDK o con `curl`, siempre con `Idempotency-Key`. Ver [Cobros](cobros.md) y [Pagos](pagos.md). 5. **Reciba el desenlace** por webhook (`payout.confirmed`, `payout.failed`, `payin.confirmed`, `payin.failed`) o consúltelo con `GET /v2/transactions/{id}`. ## Las tres reglas que conviene entender antes de escribir código - **`Idempotency-Key` es obligatoria y es su red.** Reenviar el mismo pedido con la misma clave devuelve LA MISMA operación. Es lo que hace seguro reintentar. Ver [Idempotencia y reintentos](idempotencia-y-reintentos.md). - **Los estados públicos son tres:** `pending`, `confirmed` y `failed`. Un `failed` siempre trae `failure.code`, un valor cerrado sobre el que puede hacer un `switch`. Un `201` no significa "cobrado": mire `status`. - **Los importes son texto** con punto decimal (`"1500.50"`), nunca números JSON. ## Un pago en tres líneas **`POST /v2/payouts`** ```bash curl -X POST https://api.tucapi.app/v2/payouts \ -H "Authorization: Bearer $LLAVE" \ -H "Idempotency-Key: orden-4821" \ -H "Content-Type: application/json" \ -d '{ "amount": "1500.50", "beneficiary": { "account_number": "04121234567", "bank_code": "0102", "document": { "number": "12345678", "type": "V" }, "name": "Nombre Apellido" }, "country": "VE", "currency": "VES", "metadata": { "orden": "B-2" }, "method": "mobile_payment", "purpose": "remittance", "reference": "remesa 456" }' ``` Respuesta `201`: ```json { "amount": "1500.50", "bank_reference": null, "country": "VE", "created_at": "2026-09-23T14:59:05Z", "created_by": "company", "currency": "VES", "failure": null, "id": "6f1c2a9e-3b4d-4c5e-8f70-1a2b3c4d5e6f", "metadata": { "orden": "A-1" }, "method": "mobile_payment", "pending_reason": "processing", "purpose": "remittance", "reference": "factura 123", "status": "pending", "type": "payout", "updated_at": "2026-09-23T14:59:05Z" } ``` ## Dónde está todo - [Empezar](empezar.md): Qué es la API v2, cómo funciona un cobro y un pago, y los cinco pasos para la primera integración. - [Autenticación](autenticacion.md): La llave de API, los alcances y el ambiente sandbox. - [Idempotencia y reintentos](idempotencia-y-reintentos.md): Cómo reintentar sin pagar dos veces; qué se reintenta y qué no. - [Cobros (pay-ins)](cobros.md): Cobrarle a una persona con código de un solo uso, por débito domiciliado, o esperar el Pago Móvil o la transferencia que ella manda. - [Pagos (payouts)](pagos.md): Pagarle a una persona por pago móvil o transferencia, el saldo, y la cancelación. - [Webhooks y eventos](webhooks.md): Cómo recibir el desenlace de cada operación, verificar la firma y consultar los eventos. - [Errores y fallos](errores.md): El sobre de error, los códigos de error de la API y los códigos de fallo de una operación. - [Catálogo](catalogo.md): Países, monedas, métodos con sus campos y límites, bancos y disponibilidad. - [Los SDK](sdks.md): Los clientes oficiales de Go, Node y Python: qué resuelven y cómo se usan. - [Servidor MCP](mcp.md): Cómo conectar un asistente de IA (Claude, ChatGPT, Gemini, Codex) a su cuenta por MCP, sólo lectura. --- # Autenticación Su llave de API, en `Authorization: Bearer `: `tuc_live_…` en producción, `tuc_test_…` en el sandbox (`https://api.tucapi.app/sandbox`). | Alcance | Permite | |---|---| | `payins:crear` | crear, confirmar y pedir código | | `payouts:crear` | crear pagos | | `operaciones:leer` | consultar operaciones y eventos | | `saldos:leer` | `GET /v2/balances` | | `webhooks:configurar` | registrar la url de avisos | El catálogo (`capabilities`, `methods`, `banks`) lo lee cualquier llave válida. ## Alcances Cada ruta exige un alcance, o cualquier llave válida para el catálogo: | Alcance | Rutas | |---|---| | `operaciones:leer` | `GET /v2/events`, `GET /v2/payins/{id}`, `GET /v2/payouts/{id}`, `GET /v2/transactions/{id}` | | `payins:crear` | `POST /v2/payins`, `POST /v2/payins/{id}/cancel`, `POST /v2/payins/{id}/confirm`, `POST /v2/payins/{id}/resend-code` | | `payouts:crear` | `POST /v2/payouts`, `POST /v2/payouts/{id}/cancel` | | `saldos:leer` | `GET /v2/balances` | | `webhooks:configurar` | `POST /v2/webhook-endpoints` | | cualquier llave válida | `GET /v2/banks`, `GET /v2/capabilities`, `GET /v2/methods` | Una llave sin el alcance recibe `403 scope_missing`. Una llave ausente o inválida, `401 unauthorized`. Los dos son errores de configuración, no de red: no los reintente. ## Sandbox y producción | | URL base | Llave | |---|---|---| | Producción | `https://api.tucapi.app` | `tuc_live_…` | | Sandbox | `https://api.tucapi.app/sandbox` | `tuc_test_…` | El mismo contrato sirve en los dos ambientes: cambian la llave y la URL base, y nada más. Una llave de prueba contra producción, o una de producción contra el sandbox, recibe `401 unauthorized`. En los SDK la URL base es una opción del cliente (`baseUrl`, `base_url`, `WithBaseURL`); en el servidor MCP, la variable `TUCAPI_API_URL`. En el sandbox **nada mueve dinero**: los proveedores son dobles que responden como los reales, con los mismos estados, códigos de fallo, tiempos, webhooks firmados y eventos. El desenlace de cada operación lo decide el documento del pagador o del beneficiario: | `document.number` | Desenlace | |---|---| | `00000001` | `failed`, `failure.code` de cuenta inválida | | `00000002` | `pending` al crear; `confirmed` en la primera consulta | | `00000004` | `failed` por fondos insuficientes del pagador | | `00000022` | `failed` por falta de domiciliación (sólo `direct_debit`) | | cualquier otro | `confirmed` | En un cobro con código (`debit_otp`) el código válido es siempre `123456`; cualquier otro deja el cobro en `failed` con `failure.code = code_rejected`, igual que en producción, y para volver a probar se crea otro cobro. Las referencias del banco son correlativas de 8 dígitos, como las de producción. Las llaves de prueba las emite TuCapi junto con las de producción, con los mismos alcances. --- # Idempotencia y reintentos ## La clave de idempotencia `POST /v2/payins` y `POST /v2/payouts` exigen la cabecera `Idempotency-Key` (8 a 128 caracteres). Reenviar el mismo pedido con la misma clave devuelve **la misma operación**, con el estado que tenga en ese momento, y no le pide nada nuevo al banco ni otro código a su usuario. La misma clave con otro cuerpo es `409 idempotency_key_reused`. Derive la clave de **su** identificador de orden (`orden-4821`), no de la hora ni de un azar: así un reintento desde un proceso nuevo manda la misma clave. Si usa un SDK y no pasa una clave, el SDK genera una, la reutiliza en cada reintento y **se la devuelve** con la operación creada: guárdela junto a su orden. ## Qué se reintenta solo, y qué no | Situación | Qué hacer | |---|---| | Error de red antes de recibir respuesta, `429`, `5xx` en un `GET` | Reintentar con espera creciente. Nada se movió. | | Lo mismo en un `POST` con `Idempotency-Key` | Reintentar **con la misma clave**. Es seguro por definición. | | `POST .../confirm`, `.../resend-code`, `.../cancel`, `/v2/webhook-endpoints` | **No reintentar a ciegas.** Consulte la operación con `GET` y decida. | | `503 temporarily_unavailable` en `.../cancel` | El pago sigue pendiente. Consulte; el sistema lo resuelve solo. | | `4xx` (validación, alcance, no encontrado) | Corrija el pedido. Reintentar igual da lo mismo. | Los SDK aplican exactamente esta tabla: tres intentos por omisión, espera exponencial con azar, y nunca un reintento que pueda mover dinero dos veces. ## Un resultado pendiente se resuelve solo Si una operación queda `pending` con `pending_reason: processing`, la API la consulta al proveedor hasta la respuesta final y le avisa por webhook. Usted no necesita consultar en bucle; si lo hace, `GET /v2/transactions/{id}` no le cuesta nada ni le pide nada al banco. --- # Cobros (pay-ins) Un cobro es dinero que entra. Hay cuatro métodos, en dos familias: - **`debit_otp`**: el banco del pagador le manda un código a su usuario; su usuario lo teclea en la pantalla de usted; usted lo confirma. Tres pasos con una persona en el medio. - **`direct_debit`**: débito domiciliado, con un `mandate` (contrato) que el pagador firmó con su banco. Un solo paso. - **`incoming_mobile_payment`** e **`incoming_transfer`**: el pagador manda el dinero **por su cuenta** (un Pago Móvil o una transferencia desde su banco) a la cuenta receptora; usted lo espera con un cobro creado antes. ## Cobro recibido, paso a paso 1. `POST /v2/payins` con `method: incoming_mobile_payment` (o `incoming_transfer`), el `amount` exacto y en `payer` **quién va a pagar**: documento, banco y, en Pago Móvil, teléfono. `201` con `status: pending` y `pending_reason: awaiting_payment`; `expires_at` dice hasta cuándo se espera (48 h por omisión; `expires_in_hours` la acorta, nunca más de 72). 2. Muéstrele a su pagador la cuenta receptora: `GET /v2/capabilities` la trae en `receiving_account` de cada método `incoming_*` (banco, teléfono o cuenta, documento y titular). 3. Su pagador paga desde su banco. Cuando llega un pago que coincide **en todo** (monto exacto, banco emisor y teléfono o documento del pagador, posterior a la creación del cobro y dentro de la ventana), el cobro pasa a `confirmed` con `bank_reference` y `confirmed_at`, su saldo sube por el neto y le llega `payin.confirmed`. 4. Si la ventana vence sin pago: `failed` / `expired` y `payin.failed`. Mientras espera, puede cancelarlo con `POST /v2/payins/{id}/cancel`. Un pago que llega sin un cobro que lo espere **no se le atribuye a nadie**: cree el cobro antes de pedirle a su pagador que pague. La comisión por un Pago Móvil recibido se descuenta del monto (el saldo sube por el neto); la transferencia recibida no tiene comisión. ## Cobro con código, paso a paso 1. `POST /v2/payins` con `method: debit_otp` → `201` con `status: pending` y `pending_reason: awaiting_code`. El banco le manda el código al pagador. `code_expires_at` dice hasta cuándo vale. 2. Su usuario teclea el código en su pantalla. 3. `POST /v2/payins/{id}/confirm` con `{"code": "…"}` → el débito se ejecuta. `confirmed` trae `bank_reference`; `failed` trae `failure.code`. **El código tiene un solo intento.** Uno equivocado falla la operación con `failure.code: code_rejected`: cree otro cobro, o pida otro código con `POST /v2/payins/{id}/resend-code` **antes** de confirmar (el anterior deja de servir; tope de cinco por operación). El código no se guarda nunca. Si el resultado no llegó en el momento, la operación queda `pending` / `processing`: la API la resuelve sola y le avisa por webhook. ## El concepto Lo que el pagador ve en su banco es `concept`, por ejemplo «Cobro TCP7K2M9Q»: lo pone el sistema con el prefijo de su empresa y seis caracteres del id de la operación. Su `reference` no viaja al banco. ## Rutas ### `POST /v2/payins` — Crear un cobro **Alcance:** `payins:crear` Crea un cobro. Con `method: debit_otp`, el banco de su usuario le manda un código y la operación queda `pending` / `awaiting_code` hasta que usted la confirme. Con `method: incoming_mobile_payment` o `incoming_transfer` usted declara el monto y quién va a pagar, y la operación queda `pending` / `awaiting_payment` hasta que ese pago llegue a la cuenta receptora (la que `GET /v2/capabilities` muestra en `receiving_account`) o venza la ventana (`expires_at`; `expires_in_hours` la acorta, hasta 72 h). Cuando llega un pago que coincide en monto, banco y pagador, la operación pasa a `confirmed` con `bank_reference` y le llega `payin.confirmed`; si vence, `failed` / `expired`. Mientras espera, puede cancelarla con `POST /v2/payins/{id}/cancel`. Requiere el alcance `payins:crear`. Los campos de `payer` son los que el método declara en `GET /v2/capabilities`; se validan contra esa misma declaración y un campo que el método no declara se rechaza (`details[].code = unknown`). **Reintentar este POST es seguro** con la misma `Idempotency-Key`: devuelve el mismo cobro y no pide otro código. | Parámetro | En | Obligatorio | Descripción | |---|---|---|---| | `Idempotency-Key` | header | **sí** | Una clave única por pedido, de 8 a 128 caracteres. La misma clave devuelve la misma operación. | | Código | Significa | |---|---| | `201` | El cobro existe. `status` dice qué pasó: con `awaiting_code` su usuario tiene que confirmarlo; con `awaiting_payment` se espera el pago hasta `expires_at`. | | `400` | El pedido no pasó la validación. `details` nombra cada campo. Códigos: `amount_out_of_range`, `bank_unsupported`, `body_invalid`, `country_unsupported`, `currency_unsupported`, `field_invalid`, `field_required`, `idempotency_key_required`, `method_unavailable`. | | `401` | La llave falta o no es válida. Códigos: `unauthorized`. | | `403` | La llave es válida pero no tiene el alcance. Códigos: `scope_missing`. | | `409` | La misma `Idempotency-Key` con otro cuerpo. Códigos: `idempotency_key_reused`. | | `503` | No se pudo procesar ahora. Reintente más tarde con la misma `Idempotency-Key`. Códigos: `temporarily_unavailable`. | ### `GET /v2/payins/{id}` — Consultar un cobro **Alcance:** `operaciones:leer` El estado actual. Requiere el alcance `operaciones:leer`. | Parámetro | En | Obligatorio | Descripción | |---|---|---|---| | `id` | path | **sí** | El `id` de la operación. | | Código | Significa | |---|---| | `200` | El cobro. | | `401` | La llave falta o no es válida. Códigos: `unauthorized`. | | `403` | La llave es válida pero no tiene el alcance. Códigos: `scope_missing`. | | `404` | No hay una operación con ese identificador en su empresa. Códigos: `not_found`. | ### `POST /v2/payins/{id}/cancel` — Cancelar un cobro que espera el pago **Alcance:** `payins:crear` Sólo para un cobro recibido (`incoming_mobile_payment` / `incoming_transfer`) que todavía está `pending` / `awaiting_payment`. Queda `failed` con `failure.code = cancelled` y le llega `payin.failed`. Un pago que llegue después ya no se le atribuye. Cualquier otro cobro, o uno que ya terminó, **409** `not_cancellable`. Requiere el alcance `payins:crear`. | Parámetro | En | Obligatorio | Descripción | |---|---|---|---| | `id` | path | **sí** | El `id` de la operación. | | Código | Significa | |---|---| | `200` | El cobro, cancelado. | | `401` | La llave falta o no es válida. Códigos: `unauthorized`. | | `403` | La llave es válida pero no tiene el alcance. Códigos: `scope_missing`. | | `404` | No hay una operación con ese identificador en su empresa. Códigos: `not_found`. | | `409` | El cobro no está esperando un pago. Códigos: `not_cancellable`. | ### `POST /v2/payins/{id}/confirm` — Confirmar con el código **Alcance:** `payins:crear` Ejecuta el débito con el código que tecleó su usuario. El código no se guarda. `confirmed` trae `bank_reference`. `failed` con `code_rejected` es un código equivocado o vencido: pida otro con `POST /v2/payins/{id}/resend-code` sobre una operación nueva, porque un cobro fallido es final. Requiere el alcance `payins:crear`. | Parámetro | En | Obligatorio | Descripción | |---|---|---|---| | `id` | path | **sí** | El `id` de la operación. | | Código | Significa | |---|---| | `200` | El resultado del débito. | | `400` | Falta el código o el cuerpo no se pudo leer. Códigos: `body_invalid`, `field_required`. | | `401` | La llave falta o no es válida. Códigos: `unauthorized`. | | `403` | La llave es válida pero no tiene el alcance. Códigos: `scope_missing`. | | `404` | No hay una operación con ese identificador en su empresa. Códigos: `not_found`. | | `409` | La operación no está esperando un código. Códigos: `code_not_expected`. | | `503` | No se pudo procesar ahora. Reintente más tarde con la misma `Idempotency-Key`. Códigos: `temporarily_unavailable`. | ### `POST /v2/payins/{id}/resend-code` — Pedir otro código **Alcance:** `payins:crear` Para cuando el mensaje no llegó. Tiene un tope por operación. Requiere el alcance `payins:crear`. | Parámetro | En | Obligatorio | Descripción | |---|---|---|---| | `id` | path | **sí** | El `id` de la operación. | | Código | Significa | |---|---| | `200` | La operación, con el nuevo vencimiento del código. | | `401` | La llave falta o no es válida. Códigos: `unauthorized`. | | `403` | La llave es válida pero no tiene el alcance. Códigos: `scope_missing`. | | `404` | No hay una operación con ese identificador en su empresa. Códigos: `not_found`. | | `409` | La operación no está esperando un código, o ya se pidieron demasiados. Códigos: `code_not_expected`, `too_many_codes`. | | `503` | No se pudo procesar ahora. Reintente más tarde con la misma `Idempotency-Key`. Códigos: `temporarily_unavailable`. | ## Los campos de un cobro | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `amount` | `string`
`^[0-9]+(\.[0-9]+)?$` | **sí** | Cadena decimal con punto y los decimales de la moneda. Nunca un número JSON. | | `country` | `string`
`^[A-Za-z]{2}$` | **sí** | ISO 3166-1 alfa-2. | | `currency` | `string`
`^[A-Za-z]{3}$` | **sí** | ISO 4217. | | `expires_in_hours` | `integer` | no | Sólo cobros recibidos: cuántas horas se espera el pago. Sin él, la ventana por omisión (48 h). Nunca más de 72. | | `mandate` | `Mandato` | no | | | `metadata` | `object` | no | Lo que usted quiera guardar, hasta 4 KB. Vuelve tal cual. | | `method` | `CodigoDeMetodo` | **sí** | | | `payer` | `Pagador` | **sí** | | | `reference` | `string` | no | Su referencia. Vuelve tal cual en la operación. | ### `payer` | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `account_number` | `string` | no | | | `account_type` | `mobile \| account` | no | | | `bank_code` | `string` | no | | | `document` | `object` | no | | | `email` | `string` | no | | | `name` | `string` | no | | | `phone` | `string` | no | | Qué campos exige cada método y cada banco lo dice `GET /v2/capabilities` (`fields`): mándelos tal cual, y sólo esos. En un cobro recibido no hay `name`: el pago se reconoce por documento, banco y teléfono. ### `mandate` (sólo `direct_debit`) | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `contract_date` | `string` | no | | | `contract_id` | `string`
`^[A-Za-z0-9]{1,30}$` | no | Alfanumérico, hasta 30: es lo que acepta el banco. | ## Ejemplos **`POST /v2/payins`** ```bash curl -X POST https://api.tucapi.app/v2/payins \ -H "Authorization: Bearer $LLAVE" \ -H "Idempotency-Key: orden-4821" \ -H "Content-Type: application/json" \ -d '{ "amount": "1500.50", "country": "VE", "currency": "VES", "metadata": { "orden": "A-1" }, "method": "debit_otp", "payer": { "account_number": "04121234567", "account_type": "mobile", "bank_code": "0102", "document": { "number": "12345678", "type": "V" }, "name": "Nombre Apellido" }, "reference": "factura 123" }' ``` Respuesta `201`: ```json { "amount": "1500.50", "bank_reference": null, "code_expires_at": "2026-09-23T15:04:05Z", "country": "VE", "created_at": "2026-09-23T14:59:05Z", "created_by": "company", "currency": "VES", "failure": null, "id": "6f1c2a9e-3b4d-4c5e-8f70-1a2b3c4d5e6f", "metadata": { "orden": "A-1" }, "method": "debit_otp", "pending_reason": "awaiting_code", "reference": "factura 123", "status": "pending", "type": "payin", "updated_at": "2026-09-23T14:59:05Z" } ``` **`POST /v2/payins/{id}/confirm`** ```bash curl -X POST https://api.tucapi.app/v2/payins/$ID/confirm \ -H "Authorization: Bearer $LLAVE" \ -H "Content-Type: application/json" \ -d '{ "code": "123456" }' ``` Respuesta `200`: ```json { "amount": "1500.50", "bank_reference": "000123456789", "confirmed_at": "2026-09-23T15:04:05Z", "country": "VE", "created_at": "2026-09-23T14:59:05Z", "created_by": "company", "currency": "VES", "failure": null, "id": "6f1c2a9e-3b4d-4c5e-8f70-1a2b3c4d5e6f", "metadata": { "orden": "A-1" }, "method": "debit_otp", "reference": "factura 123", "status": "confirmed", "type": "payin", "updated_at": "2026-09-23T14:59:05Z" } ``` **`POST /v2/payins/{id}/cancel`** ```bash curl -X POST https://api.tucapi.app/v2/payins/$ID/cancel \ -H "Authorization: Bearer $LLAVE" ``` Respuesta `200`: ```json { "amount": "1500.50", "bank_reference": null, "country": "VE", "created_at": "2026-09-23T14:59:05Z", "created_by": "company", "currency": "VES", "expires_at": null, "failure": { "code": "cancelled", "message": "La operación fue cancelada." }, "id": "6f1c2a9e-3b4d-4c5e-8f70-1a2b3c4d5e6f", "metadata": { "orden": "A-1" }, "method": "incoming_mobile_payment", "pending_reason": null, "reference": "factura 123", "status": "failed", "type": "payin", "updated_at": "2026-09-23T14:59:05Z" } ``` --- # Pagos (payouts) Un pago le manda dinero a una persona desde su saldo. Métodos: **`mobile_payment`** (pago móvil, por teléfono) y **`bank_transfer`** (transferencia a cuenta). El pago sale en el momento; el desenlace llega por webhook. ## Saldo Un pago reserva el monto de su saldo disponible al crearse (`available` → `reserved`), lo captura al confirmarse y lo libera si falla. El saldo es contable, no un freno: el pago sale aunque `available` quede en negativo. `GET /v2/balances` le muestra `available` y `reserved` por moneda. ## Cancelar `POST /v2/payouts/{id}/cancel` cancela un pago que todavía está pendiente. - Cancelado → `status: failed`, `failure.code: cancelled`, saldo liberado. - Ya no se puede → `409 not_cancellable` (mire el estado: probablemente ya se confirmó). - El proveedor no contestó claro → `503 temporarily_unavailable`: el pago **sigue pendiente**, nada cambió, y no lo reintente a ciegas: consulte. ## Rutas ### `POST /v2/payouts` — Crear un pago **Alcance:** `payouts:crear` Crea un pago y lo envía. Es UN paso: el monto se reserva de su saldo disponible al crearlo y se descuenta al confirmarse; si el pago falla, vuelve a estar disponible. Requiere el alcance `payouts:crear`. Los campos de `beneficiary` son los que el método declara en `GET /v2/capabilities`. `purpose` es opcional: `remittance` por omisión. El resultado suele llegar después: la operación queda `pending` / `processing` y el desenlace le llega por webhook (`payout.confirmed` / `payout.failed`) o consultándola. | Parámetro | En | Obligatorio | Descripción | |---|---|---|---| | `Idempotency-Key` | header | **sí** | Una clave única por pedido, de 8 a 128 caracteres. La misma clave devuelve la misma operación. | | Código | Significa | |---|---| | `201` | El pago existe. `status` dice qué pasó. | | `400` | El pedido no pasó la validación. `details` nombra cada campo. Códigos: `amount_out_of_range`, `bank_unsupported`, `body_invalid`, `country_unsupported`, `currency_unsupported`, `field_invalid`, `field_required`, `idempotency_key_required`, `method_unavailable`. | | `401` | La llave falta o no es válida. Códigos: `unauthorized`. | | `403` | La llave es válida pero no tiene el alcance. Códigos: `scope_missing`. | | `409` | La misma `Idempotency-Key` con otro cuerpo. Códigos: `idempotency_key_reused`. | | `503` | No se pudo procesar ahora. Reintente más tarde con la misma `Idempotency-Key`. Códigos: `temporarily_unavailable`. | ### `GET /v2/payouts/{id}` — Consultar un pago **Alcance:** `operaciones:leer` El estado actual. Requiere el alcance `operaciones:leer`. | Parámetro | En | Obligatorio | Descripción | |---|---|---|---| | `id` | path | **sí** | El `id` de la operación. | | Código | Significa | |---|---| | `200` | El pago. | | `401` | La llave falta o no es válida. Códigos: `unauthorized`. | | `403` | La llave es válida pero no tiene el alcance. Códigos: `scope_missing`. | | `404` | No hay una operación con ese identificador en su empresa. Códigos: `not_found`. | ### `POST /v2/payouts/{id}/cancel` — Cancelar un pago pendiente **Alcance:** `payouts:crear` Pide cancelar un pago que todavía está `pending` / `processing`. Sólo si el último estado conocido lo permite; si el pago ya se ejecutó o ya terminó, **409** `not_cancellable`. Cancelado, el pago queda `failed` con `failure.code = cancelled`, el monto vuelve a su saldo disponible y le llega `payout.failed`. Si no se pudo confirmar la cancelación en el momento, **503** `temporarily_unavailable`: el pago sigue pendiente y puede volver a intentar. Requiere el alcance `payouts:crear`. | Parámetro | En | Obligatorio | Descripción | |---|---|---|---| | `id` | path | **sí** | El `id` de la operación. | | Código | Significa | |---|---| | `200` | El pago, cancelado. | | `401` | La llave falta o no es válida. Códigos: `unauthorized`. | | `403` | La llave es válida pero no tiene el alcance. Códigos: `scope_missing`. | | `404` | No hay una operación con ese identificador en su empresa. Códigos: `not_found`. | | `409` | El pago ya no se puede cancelar. Códigos: `not_cancellable`. | | `503` | La cancelación no se pudo confirmar ahora. El pago sigue pendiente; reintente. Códigos: `temporarily_unavailable`. | ## Los campos de un pago | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `amount` | `string`
`^[0-9]+(\.[0-9]+)?$` | **sí** | Cadena decimal con punto y los decimales de la moneda. | | `beneficiary` | `Beneficiario` | **sí** | | | `country` | `string`
`^[A-Za-z]{2}$` | **sí** | ISO 3166-1 alfa-2. | | `currency` | `string`
`^[A-Za-z]{3}$` | **sí** | ISO 4217. | | `metadata` | `object` | no | Lo que usted quiera guardar, hasta 4 KB. Vuelve tal cual. | | `method` | `CodigoDeMetodo` | **sí** | | | `purpose` | `Proposito` | no | | | `reference` | `string` | no | Su referencia. Vuelve tal cual en la operación. | ### `beneficiary` | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `account_number` | `string` | no | | | `bank_code` | `string` | no | | | `document` | `object` | no | | | `name` | `string` | no | | `purpose` es `remittance` por omisión; `payroll` para nómina. `beneficiary.name` es opcional: si usted lo manda, se usa; si no, el sistema lo completa con el documento («Titular V-12345678») antes de mandar el pago. **El concepto lo pone el sistema.** Lo que el beneficiario ve en su movimiento bancario es `concept`, por ejemplo «Pago TCP7K2M9Q»: «Pago», el prefijo de tres letras de su empresa y seis caracteres tomados del id de la operación. Usted no lo manda ni lo cambia; viene en la operación para que pueda mostrárselo a su usuario o buscarlo en un extracto. Su `reference` y su `metadata` se guardan y vuelven tal cual, pero no viajan al banco. ## Ejemplos **`POST /v2/payouts`** ```bash curl -X POST https://api.tucapi.app/v2/payouts \ -H "Authorization: Bearer $LLAVE" \ -H "Idempotency-Key: orden-4821" \ -H "Content-Type: application/json" \ -d '{ "amount": "1500.50", "beneficiary": { "account_number": "04121234567", "bank_code": "0102", "document": { "number": "12345678", "type": "V" }, "name": "Nombre Apellido" }, "country": "VE", "currency": "VES", "metadata": { "orden": "B-2" }, "method": "mobile_payment", "purpose": "remittance", "reference": "remesa 456" }' ``` Respuesta `201`: ```json { "amount": "1500.50", "bank_reference": null, "country": "VE", "created_at": "2026-09-23T14:59:05Z", "created_by": "company", "currency": "VES", "failure": null, "id": "6f1c2a9e-3b4d-4c5e-8f70-1a2b3c4d5e6f", "metadata": { "orden": "A-1" }, "method": "mobile_payment", "pending_reason": "processing", "purpose": "remittance", "reference": "factura 123", "status": "pending", "type": "payout", "updated_at": "2026-09-23T14:59:05Z" } ``` **`POST /v2/payouts/{id}/cancel`** ```bash curl -X POST https://api.tucapi.app/v2/payouts/$ID/cancel \ -H "Authorization: Bearer $LLAVE" ``` Respuesta `200`: ```json { "amount": "1500.50", "bank_reference": null, "country": "VE", "created_at": "2026-09-23T14:59:05Z", "created_by": "company", "currency": "VES", "failure": { "code": "cancelled", "message": "La operación fue cancelada." }, "id": "6f1c2a9e-3b4d-4c5e-8f70-1a2b3c4d5e6f", "metadata": { "orden": "A-1" }, "method": "mobile_payment", "pending_reason": null, "purpose": "remittance", "reference": "factura 123", "status": "failed", "type": "payout", "updated_at": "2026-09-23T14:59:05Z" } ``` --- # Webhooks y eventos Cada operación que llega a un estado final emite **un evento**: `payin.confirmed`, `payin.failed`, `payout.confirmed` o `payout.failed` (y `test.ping` es el aviso de prueba, abajo). El evento lleva la operación entera en `data`. Se lo mandamos a su url por webhook, y además puede consultarlo con `GET /v2/events`. ## Registrar su url `POST /v2/webhook-endpoints` con `{"url": "https://…"}` devuelve el **secreto una sola vez**: guárdelo. Una url activa por empresa: registrar otra la reemplaza, y el secreto anterior sigue valiendo 24 horas para que cambie su copia sin perder ningún aviso. ## Verificar la firma Cada aviso llega con estas cabeceras: | Cabecera | Qué trae | |---|---| | `X-Firma` | la firma, `v1=`; durante una rotación van dos separadas por espacio | | `X-Timestamp` | segundos Unix de cuándo se firmó; se rechaza fuera de la ventana | | `X-Evento` | el `type` del evento | | `X-Id-Evento` | el `id` del evento, para deduplicar | Algoritmo: `HMAC-SHA256` sobre ` + "." + `, en `hex`; tolerancia de reloj: 300 segundos. La firma es **HMAC-SHA256** sobre `X-Timestamp + "." + cuerpo crudo`, en hexadecimal, con su secreto como llave, con el prefijo `v1=`. Para verificarla: 1. Lea el cuerpo **crudo**, antes de parsear el JSON: reserializar cambia los bytes y la firma deja de coincidir. 2. Rechace si `X-Timestamp` está a más de 5 minutos de su reloj: sin esto, un aviso legítimo capturado una vez sirve para siempre. 3. Calcule el HMAC y compárelo en **tiempo constante** con cada firma de `X-Firma` (durante una rotación de secreto van dos, separadas por espacio; alcanza con que una coincida). 4. Sólo entonces parsee el JSON y procese el evento. Deduplique por `id`. Los tres SDK lo hacen por usted (`ParseEvent`, `parseEvent`, `parse_event`). Es la parte del SDK que más vale la pena usar: verificar mal una firma es el error que no se nota. ## Probar su url Desde el dashboard (**Webhooks → Enviar aviso de prueba**) le mandamos un aviso `test.ping` firmado igual que los de verdad, con las mismas cabeceras y el secreto vigente. Sirve para probar que su url contesta y que su código verifica la firma antes de que llegue el primer pago. Su `data` es una operación de **ejemplo**: el `id` en ceros, `status` `pending`, `amount` `0.00` y `metadata.test = true`. No corresponde a ningún movimiento. Decida siempre por `type`: conteste `2xx` a `test.ping` y no lo procese. No se reintenta ni aparece en `GET /v2/events`. ## Entrega y reintentos Conteste `2xx` en menos de 10 segundos. Si no, reintentamos con espera creciente durante unas 24 horas (30 s, 2, 10, 30 min, 1, 2, 4, 8 y 8 h) y recién entonces el aviso se da por agotado; un aviso agotado se reenvía a pedido. Un mismo evento puede llegarle más de una vez: por eso el `id`. ## Consultar los eventos `GET /v2/events?cursor=&limit=` devuelve sus eventos en orden con un cursor: sirve para reconciliar, para recuperar lo que no recibió y para integrar sin webhook. Guarde `next_cursor`. ## Rutas ### `GET /v2/events` — Los eventos de sus operaciones **Alcance:** `operaciones:leer` El mismo evento que viaja por webhook, en orden, con cursor. Sirve para reponerse de un webhook perdido: guarde el último `next_cursor` y pida desde ahí. Requiere el alcance `operaciones:leer`. | Parámetro | En | Obligatorio | Descripción | |---|---|---|---| | `cursor` | query | no | El `next_cursor` de la respuesta anterior. Sin él, desde el principio. | | `limit` | query | no | | | Código | Significa | |---|---| | `200` | Los eventos. | | `400` | Un parámetro inválido. Códigos: `field_invalid`. | | `401` | La llave falta o no es válida. Códigos: `unauthorized`. | | `403` | La llave es válida pero no tiene el alcance. Códigos: `scope_missing`. | ### `GET /v2/transactions/{id}` — Consultar cualquier operación **Alcance:** `operaciones:leer` Un cobro o un pago por su `id`, con la misma forma. Requiere el alcance `operaciones:leer`. | Parámetro | En | Obligatorio | Descripción | |---|---|---|---| | `id` | path | **sí** | El `id` de la operación. | | Código | Significa | |---|---| | `200` | La operación. | | `401` | La llave falta o no es válida. Códigos: `unauthorized`. | | `403` | La llave es válida pero no tiene el alcance. Códigos: `scope_missing`. | | `404` | No hay una operación con ese identificador en su empresa. Códigos: `not_found`. | ### `POST /v2/webhook-endpoints` — Registrar la url de avisos **Alcance:** `webhooks:configurar` Registra la url a la que se le mandan los eventos y devuelve el **secreto de firma una sola vez**. Una url activa por empresa: registrar otra la reemplaza, y el secreto anterior sigue valiendo 24 horas para que el cambio no pierda avisos. Requiere el alcance `webhooks:configurar`. La url tiene que ser `https`. ## Cómo llega un evento `POST` a su url con el cuerpo de `Evento` y estas cabeceras: `X-Firma` (HMAC-SHA256 de `.` con su secreto, como `v1=`; durante una rotación van dos, separadas por espacio), `X-Timestamp` (segundos Unix), `X-Evento` (el `type`) y `X-Id-Evento` (el `id`). Conteste 2xx en menos de 10 segundos; si no, se reintenta con espera creciente durante unas 24 horas (30 s, 2, 10, 30 min, 1, 2, 4, 8 y 8 h, con un ±10 % al azar) y recién entonces se agota; un aviso agotado se reenvía a mano. Deduplique por `id`. Mientras no registre nada acá, los eventos van a la url y con el secreto de la versión 1, si los tiene. | Código | Significa | |---|---| | `201` | Registrada. Guarde `secret`: no se vuelve a mostrar. | | `400` | Falta la url o no es https. Códigos: `body_invalid`, `field_invalid`, `field_required`. | | `401` | La llave falta o no es válida. Códigos: `unauthorized`. | | `403` | La llave es válida pero no tiene el alcance. Códigos: `scope_missing`. | ## El evento | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `data` | `Operacion` | **sí** | | | `id` | `string` | **sí** | | | `occurred_at` | `string` | **sí** | | | `type` | `TipoDeEvento` | **sí** | | **`POST /v2/webhook-endpoints`** ```bash curl -X POST https://api.tucapi.app/v2/webhook-endpoints \ -H "Authorization: Bearer $LLAVE" \ -H "Content-Type: application/json" \ -d '{ "url": "https://api.suempresa.com/avisos" }' ``` --- # Errores y fallos Hay dos cosas distintas y conviene no mezclarlas: - **Un error de la API** (`4xx`/`5xx`): el pedido no se pudo atender. Llega siempre en el mismo sobre: `{"error": {"code", "message", "details"}}`. Compare `code`, muestre `message`. `details` sólo viene en los errores de validación, con un ítem por campo. - **Un fallo de la operación** (`status: failed`): el pedido se atendió, la operación existe, y no salió. `failure.code` dice por qué y es un valor cerrado, fijo, para decidir; `failure.reason` es el motivo normalizado debajo del código, para explicar; `failure.message` es un texto legible que depende del motivo. Nunca traen el código crudo del banco ni del proveedor. ## Códigos de error - `unauthorized`: Falta la cabecera Authorization o la llave de API no es válida. - `scope_missing`: Su llave no tiene permiso para esta operación. - `body_invalid`: El cuerpo del pedido no se pudo leer. - `field_required`: Faltan campos obligatorios. - `field_invalid`: Hay campos con un valor inválido. - `method_unavailable`: El método no está disponible para ese país y moneda. - `bank_unsupported`: El banco indicado no admite este método. - `amount_out_of_range`: El monto está fuera del rango permitido. - `currency_unsupported`: La moneda no está disponible. - `country_unsupported`: El país no está disponible. - `idempotency_key_reused`: La clave de idempotencia ya se usó con otro pedido. - `idempotency_key_required`: Falta la cabecera Idempotency-Key. - `not_found`: No existe una operación con ese identificador. - `not_cancellable`: La operación ya no se puede cancelar. - `code_not_expected`: La operación no está esperando un código. - `too_many_codes`: Se pidieron demasiados códigos para esta operación. - `temporarily_unavailable`: No se pudo procesar en este momento. Intente más tarde. - `route_not_found`: La ruta no existe. - `method_not_allowed`: El método HTTP no está permitido en esta ruta. ## Códigos de fallo - `counterparty_rejected`: El banco del destinatario rechazó la operación. - `payer_insufficient_funds`: El pagador no tiene fondos suficientes. - `mandate_required`: El pagador todavía no autorizó el débito en su banco. - `code_rejected`: El código no es válido o venció. Pida uno nuevo. - `limit_exceeded`: El monto supera el límite permitido. - `outside_hours`: Fuera del horario bancario. - `invalid_data`: El banco no aceptó los datos de la operación. Revíselos y cree otra. - `expired`: La operación venció. - `cancelled`: La operación fue cancelada. - `temporarily_unavailable`: No se pudo procesar en este momento. Intente más tarde. - `rejected`: La operación fue rechazada. Motivos que admite cada código (`failure.reason`): `counterparty_rejected` → `invalid_account`, `account_closed`, `account_blocked`, `beneficiary_mismatch`, `invalid_phone`, `invalid_bank`, `invalid_document`, `other`; `payer_insufficient_funds` → `payer_insufficient_funds`; `mandate_required` → `no_mandate`, `mandate_revoked`; `code_rejected` → `code_invalid`, `code_expired`; `limit_exceeded` → `amount_over_limit`, `daily_limit`; `outside_hours` → `outside_hours`; `invalid_data` → `invalid_data`; `temporarily_unavailable` → `other`, `bank_offline`, `timeout`; `expired` → `expired`; `cancelled` → `cancelled`; `rejected` → `other`. ## Motivos de fallo Cada código admite un conjunto fijo de motivos (`failure.reason`). `other` es «sin más detalle»: ahí el mensaje es el del código. El motivo normalizado de un fallo, con el código al que pertenece: - `invalid_account` (counterparty_rejected): La cuenta o el teléfono del destinatario no existe en su banco. - `account_closed` (counterparty_rejected): La cuenta del destinatario está cerrada. - `account_blocked` (counterparty_rejected): La cuenta del destinatario está bloqueada. - `beneficiary_mismatch` (counterparty_rejected): Los datos del destinatario no corresponden a esa cuenta. - `invalid_phone` (counterparty_rejected): El teléfono del destinatario no es válido para Pago Móvil. - `invalid_bank` (counterparty_rejected): El banco del destinatario no existe o no recibe este tipo de operación. - `invalid_document` (counterparty_rejected): El documento del destinatario no es válido. - `other` (counterparty_rejected, temporarily_unavailable, rejected): Sin más detalle: vale el mensaje del código. - `payer_insufficient_funds` (payer_insufficient_funds): El pagador no tiene fondos suficientes. - `code_invalid` (code_rejected): El código no es válido. - `code_expired` (code_rejected): El código venció. - `no_mandate` (mandate_required): El pagador todavía no autorizó el débito en su banco. - `mandate_revoked` (mandate_required): El pagador suspendió o revocó la autorización de débito. - `amount_over_limit` (limit_exceeded): El monto supera el límite permitido para esta operación. - `daily_limit` (limit_exceeded): Se superó el límite diario permitido. - `outside_hours` (outside_hours): Fuera del horario bancario. - `invalid_data` (invalid_data): El banco no aceptó los datos de la operación. - `bank_offline` (temporarily_unavailable): El banco no está disponible en este momento. - `timeout` (temporarily_unavailable): El banco no respondió a tiempo. - `expired` (expired): La operación venció. - `cancelled` (cancelled): La operación fue cancelada. ## Los detalles de validación | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `code` | `CodigoDeDetalle` | **sí** | | | `field` | `string` | **sí** | La ruta del campo, con punto: `payer.document.number`, `Idempotency-Key`. | `code` de un detalle: `required`, `invalid`, `unknown`. ## El sobre | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `code` | `CodigoDeError` | **sí** | | | `details` | `array` | no | Sólo en los errores de validación: un ítem por campo. | | `message` | `string` | **sí** | Para mostrar, no para comparar. | --- # Catálogo El catálogo dice **qué se puede hacer hoy** con su llave. Léalo al arrancar y cuando cambie `config_version`; no lo escriba en su código. - `GET /v2/capabilities`: todo junto: países, monedas, métodos de cobro y de pago por país con sus campos (`fields`), límites (`min_amount`, `max_amount`), disponibilidad y, en los cobros recibidos (`incoming_*`), la cuenta receptora (`receiving_account`) que usted le muestra a su pagador. - `GET /v2/methods?country=&direction=`: sólo los métodos. - `GET /v2/banks?country=&method=`: los bancos de un país con los métodos que admiten hoy. Nunca dice por qué proveedor. ## Disponibilidad - `available` - `temporarily_unavailable` - `disabled` Un método `temporarily_unavailable` vuelve solo; uno `disabled` lo apagó un operador. Crear una operación con un método no disponible es `503 temporarily_unavailable` o `400 method_unavailable`. ## Rutas ### `GET /v2/balances` — Sus saldos por moneda **Alcance:** `saldos:leer` Por moneda: `available` y `reserved`. Requiere el alcance `saldos:leer`. | Código | Significa | |---|---| | `200` | Los saldos. | | `401` | La llave falta o no es válida. Códigos: `unauthorized`. | | `403` | La llave es válida pero no tiene el alcance. Códigos: `scope_missing`. | ### `GET /v2/banks` — Los bancos de un país **Alcance:** cualquier llave válida Con `code`, `name` y qué métodos admite hoy cada uno. Requiere una llave válida, sin alcance particular. | Parámetro | En | Obligatorio | Descripción | |---|---|---|---| | `country` | query | **sí** | ISO 3166-1 alfa-2. | | `method` | query | no | Sólo los bancos que admiten este método. | | Código | Significa | |---|---| | `200` | Los bancos. | | `400` | Un parámetro inválido o faltante. Códigos: `country_unsupported`, `field_required`, `method_unavailable`. | | `401` | La llave falta o no es válida. Códigos: `unauthorized`. | ### `GET /v2/capabilities` — Qué puede hacer su llave hoy **Alcance:** cualquier llave válida Países, monedas, métodos por dirección con sus campos, límites y disponibilidad, y `config_version`. Es la única fuente que necesita leer para saber qué puede hacer hoy. Requiere una llave válida, sin alcance particular. | Código | Significa | |---|---| | `200` | Las capacidades. | | `401` | La llave falta o no es válida. Códigos: `unauthorized`. | ### `GET /v2/methods` — El catálogo de métodos **Alcance:** cualquier llave válida Filtrable por país y dirección. Requiere una llave válida, sin alcance particular. | Parámetro | En | Obligatorio | Descripción | |---|---|---|---| | `country` | query | no | ISO 3166-1 alfa-2. Sin él, todos los países. | | `direction` | query | no | Sin él, las dos direcciones. | | Código | Significa | |---|---| | `200` | Los métodos. | | `400` | Un parámetro inválido. Códigos: `country_unsupported`, `field_invalid`. | | `401` | La llave falta o no es válida. Códigos: `unauthorized`. | ## Las formas | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `availability` | `Disponibilidad` | **sí** | | | `code` | `CodigoDeMetodo` | **sí** | | | `country` | `string` | **sí** | | | `currency` | `string` | **sí** | | | `direction` | `payin \| payout` | **sí** | | | `fields` | `array` | **sí** | Lo que `payer` (o `beneficiary`) tiene que traer para este método. | | `max_amount` | `string \| null` | **sí** | `null` = sin tope. | | `min_amount` | `string` | **sí** | | | `name` | `string` | **sí** | | | `receiving_account` | `CuentaReceptora \| null` | no | Sólo cobros recibidos: adónde tiene que pagar su pagador. Muéstreselo tal cual. Ausente si todavía no está cargada. | ### Un campo de `fields` | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `enum` | `array` | no | | | `example` | `string` | no | | | `name` | `string` | **sí** | La ruta dentro de `payer`, con punto para los anidados. | | `pattern` | `string` | no | | | `required` | `boolean` | **sí** | | | `type` | `string \| enum \| date` | **sí** | | ### La cuenta receptora (sólo `incoming_*`) | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `account_number` | `string` | no | Sólo `incoming_transfer`: la cuenta de 20 dígitos. | | `bank_code` | `string` | **sí** | El banco de la cuenta receptora. | | `document` | `string` | **sí** | El documento del titular, como lo pide el banco emisor. | | `holder` | `string` | **sí** | El nombre del titular. | | `phone` | `string` | no | Sólo `incoming_mobile_payment`: el teléfono al que se manda el Pago Móvil. | ### Banco | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `code` | `string` | **sí** | | | `methods` | `array` | **sí** | Los que admite hoy. | | `name` | `string` | **sí** | | ### Saldo | Campo | Tipo | Obligatorio | Descripción | |---|---|---|---| | `available` | `string` | **sí** | | | `currency` | `string` | **sí** | | | `reserved` | `string` | **sí** | | --- # Los SDK Hay tres clientes oficiales, en Go, Node y Python. Los tres hacen lo mismo: 1. Autenticación con su llave. 2. El contrato, tipado: los tipos, los enums y las rutas se generan desde este mismo OpenAPI, así que no pueden desviarse de la API. 3. **Idempotencia y reintentos seguros**: cada creación lleva `Idempotency-Key` (la suya, o una que el SDK genera, reutiliza en cada reintento y le devuelve), y el SDK sólo reintenta lo que no puede mover dinero dos veces. 4. **Verificación de la firma de los webhooks**, en tiempo constante, con ventana de reloj y sobre el cuerpo crudo. Ninguno tiene dependencias fuera de la biblioteca estándar de su lenguaje. Por omisión hablan con producción (`https://api.tucapi.app`). Para el sandbox se pasa la URL base `https://api.tucapi.app/sandbox` con la llave de prueba: `tucapi.New("tuc_test_...", tucapi.WithBaseURL("https://api.tucapi.app/sandbox"))`, `new TuCapi('tuc_test_...', { baseUrl: 'https://api.tucapi.app/sandbox' })`, `TuCapi("tuc_test_...", base_url="https://api.tucapi.app/sandbox")`. ## Instalar | | Paquete | Instalar | Cliente | |---|---|---|---| | Go | `github.com/Yhonnyj/pagos-sdk-go/v2` | `go get github.com/Yhonnyj/pagos-sdk-go/v2` | `tucapi.New("tuc_live_...")` | | Node | `tucapi` (npm) | `npm install tucapi` | `new TuCapi('tuc_live_...')` | | Python | `tucapi` (PyPI) | `pip install tucapi` | `TuCapi("tuc_live_...")` | ## La misma forma en los tres | Operación | Go | Node | Python | |---|---|---|---| | `GET /v2/balances` | `c.Balances.List(ctx, …)` | `c.balances.list(…)` | `c.balances.list(…)` | | `GET /v2/banks` | `c.Banks.List(ctx, …)` | `c.banks.list(…)` | `c.banks.list(…)` | | `GET /v2/capabilities` | `c.Capabilities.Get(ctx, …)` | `c.capabilities.get(…)` | `c.capabilities.get(…)` | | `GET /v2/events` | `c.Events.List(ctx, …)` | `c.events.list(…)` | `c.events.list(…)` | | `GET /v2/methods` | `c.Methods.List(ctx, …)` | `c.methods.list(…)` | `c.methods.list(…)` | | `POST /v2/payins` | `c.Payins.Create(ctx, …)` | `c.payins.create(…)` | `c.payins.create(…)` | | `GET /v2/payins/{id}` | `c.Payins.Get(ctx, …)` | `c.payins.get(…)` | `c.payins.get(…)` | | `POST /v2/payins/{id}/cancel` | `c.Payins.Cancel(ctx, …)` | `c.payins.cancel(…)` | `c.payins.cancel(…)` | | `POST /v2/payins/{id}/confirm` | `c.Payins.Confirm(ctx, …)` | `c.payins.confirm(…)` | `c.payins.confirm(…)` | | `POST /v2/payins/{id}/resend-code` | `c.Payins.ResendCode(ctx, …)` | `c.payins.resendCode(…)` | `c.payins.resend_code(…)` | | `POST /v2/payouts` | `c.Payouts.Create(ctx, …)` | `c.payouts.create(…)` | `c.payouts.create(…)` | | `GET /v2/payouts/{id}` | `c.Payouts.Get(ctx, …)` | `c.payouts.get(…)` | `c.payouts.get(…)` | | `POST /v2/payouts/{id}/cancel` | `c.Payouts.Cancel(ctx, …)` | `c.payouts.cancel(…)` | `c.payouts.cancel(…)` | | `GET /v2/transactions/{id}` | `c.Transactions.Get(ctx, …)` | `c.transactions.get(…)` | `c.transactions.get(…)` | | `POST /v2/webhook-endpoints` | `c.WebhookEndpoints.Create(ctx, …)` | `c.webhookEndpoints.create(…)` | `c.webhook_endpoints.create(…)` | ## Ejemplo **Go** ```go c := tucapi.New("tuc_live_...") created, err := c.Payouts.Create(ctx, tucapi.NewPayout{ Country: "VE", Currency: "VES", Method: tucapi.MethodCodeMobilePayment, Amount: "1500.50", Beneficiary: tucapi.Beneficiary{Name: "Ana Pérez", Document: &tucapi.Document{Type: "V", Number: "12345678"}, BankCode: "0102", AccountNumber: "04121234567"}, }, tucapi.WithIdempotencyKey("orden-4821")) // created.IdempotencyKey es la clave que se mandó ``` **Node** ```ts const c = new TuCapi('tuc_live_...') const created = await c.payouts.create( { country: 'VE', currency: 'VES', method: 'mobile_payment', amount: '1500.50', beneficiary: { name: 'Ana Pérez', document: { type: 'V', number: '12345678' }, bank_code: '0102', account_number: '04121234567' } }, { idempotencyKey: 'orden-4821' }, ) // created.idempotencyKey es la clave que se mandó ``` **Python** ```python c = TuCapi("tuc_live_...") created = c.payouts.create( {"country": "VE", "currency": "VES", "method": "mobile_payment", "amount": "1500.50", "beneficiary": {"name": "Ana Pérez", "document": {"type": "V", "number": "12345678"}, "bank_code": "0102", "account_number": "04121234567"}}, idempotency_key="orden-4821", ) # created["idempotency_key"] es la clave que se mandó ``` ## Webhooks | | Go | Node | Python | |---|---|---|---| | Verificar y leer un aviso | `tucapi.ParseEvent(r, secret)` | `parseEvent(rawBody, req.headers, secret)` | `parse_event(raw_body, headers, secret)` | | Sobre bytes ya leídos | `tucapi.VerifyEvent(body, sig, ts, secret)` | `verifyEvent(body, sig, ts, secret)` | `verify_event(body, sig, ts, secret)` | | Si no verifica | `tucapi.ErrInvalidSignature` | `SignatureError` | `SignatureError` | --- # Servidor MCP TuCapi expone un servidor **MCP** (Model Context Protocol) para que un asistente de IA consulte su cuenta con su propia llave: el catálogo, sus saldos, cualquier operación por id y sus eventos. **Sólo lectura**: ninguna herramienta crea, confirma ni cancela operaciones. Las herramientas de escritura llegarán detrás de un alcance propio y de confirmación explícita. ## Conectarse - **Por HTTP** (ChatGPT, Gemini CLI por url, cualquier cliente MCP con transporte streamable HTTP): `POST /mcp`, con su llave en `Authorization: Bearer `. La llave viaja en cada pedido y determina su empresa; el servidor no guarda llaves. - **Por stdio** (Claude Code, Gemini CLI, Codex, en su máquina): el binario `mcp` con las variables `TUCAPI_API_URL` y `TUCAPI_API_KEY`. Para el sandbox, `TUCAPI_API_URL=https://api.tucapi.app/sandbox` con una llave `tuc_test_…`. ## Herramientas | Herramienta | Ruta | Qué devuelve | |---|---|---| | `balances_list` | `GET /v2/balances` | Sus saldos por moneda | | `banks_list` | `GET /v2/banks` | Los bancos de un país | | `capabilities_get` | `GET /v2/capabilities` | Qué puede hacer su llave hoy | | `events_list` | `GET /v2/events` | Los eventos de sus operaciones | | `methods_list` | `GET /v2/methods` | El catálogo de métodos | | `payins_get` | `GET /v2/payins/{id}` | Consultar un cobro | | `payouts_get` | `GET /v2/payouts/{id}` | Consultar un pago | | `transactions_get` | `GET /v2/transactions/{id}` | Consultar cualquier operación | Cada herramienta devuelve el mismo JSON que la ruta correspondiente de la API; los errores llegan como texto con el código estable. ## Recursos - `tucapi://openapi`: este contrato. - `tucapi://llms.txt`: el índice de esta documentación. - `tucapi://docs/`: cada página de esta documentación, en Markdown. ## Paquetes de integración listos - **Claude**: el Agent Skill `tucapi-pagos-v2` (`SKILL.md` con referencias) en `integraciones/claude/`, también como `.zip`. - **ChatGPT**: el mismo skill y las instrucciones para conectar el servidor MCP en modo desarrollador, en `integraciones/chatgpt/`. - **Gemini CLI**: una extensión (`gemini-extension.json`, `GEMINI.md` y el skill) en `integraciones/gemini/`. Los tres se generan de esta misma documentación. ---