Documentación de la API
Última actualización: 23 de septiembre de 2026
Micalpays expone una API REST para mover capital en cripto en las dos direcciones: pedir pagos salientes en stablecoin (payouts) y cobrar en stablecoin con direcciones de depósito propias (payin), y avisa por webhook firmado cada vez que un pago o un cobro cambia de estado. Esta página es el contrato completo: lo que aparece acá se puede dar por estable; cualquier campo o comportamiento que no esté documentado puede cambiar sin previo aviso.
Todas las respuestas son JSON. Las fechas van en ISO-8601 con zona UTC. Los montos son enteros en la unidad más chica del token, siempre como string, nunca como número de punto flotante.
1. Base URL y convenciones
Base URL: https://micalpays.sbs/api/v1
Enviar y aceptar JSON (Content-Type: application/json, Accept: application/json). Los identificadores públicos son el Business ID de tu cuenta, la external_reference de cada payout, el label y la reference (MPD-…) de cada dirección de depósito y la reference (MPD-TX-…) de cada depósito; la API nunca expone IDs internos.
Las llamadas están limitadas a 120 peticiones por minuto por cuenta. Al superarlas la API responde 429 y conviene esperar el valor del header Retry-After antes de reintentar. Algunos endpoints tienen además un límite propio: el saldo vivo de una dirección de depósito (10 por minuto), la conciliación por hash (5 por minuto), el estado de cuenta GET /v1/statement (12 por minuto) y el catálogo público GET /v1/networks (60 por minuto por IP, sin API Key). Las respuestas que se calculan a partir de la cadena o de todo el historial se sirven con un cache corto: los saldos de GET /v1/wallet y de /balances se leen de la cadena cada 60 segundos (updated_at lo dice), la necesidad y la deuda de GET /v1/wallet se recalculan cada 15 segundos y GET /v1/statement cada 30 (generated_at). GET /v1/payouts/{external_reference} y GET /v1/deposits/{reference} siempre leen el estado actual.
2. Autenticación
Cada petición lleva tu API Key (la misma con la que entras al portal). Se acepta como Bearer token en el header Authorization o, alternativamente, en el header X-Api-Key.
La API Key se muestra una sola vez al crear la cuenta o al rotarla desde el portal; solo guardamos un hash. Rotarla invalida la anterior en el acto, salvo que al rotar elijas un período de gracia (1 o 24 horas): durante ese plazo las dos llaves autentican y cada respuesta obtenida con la anterior lleva los headers Deprecation: true y X-Api-Key-Expires-At (ISO-8601 UTC), para que despliegues la nueva sin cortes.
GET https://micalpays.sbs/api/v1/payouts/order-1234
Authorization: Bearer mp_live_XXXXXXXXXXXXXXXXXXXXXXXX
Accept: application/json
- 401 — falta la API Key o no corresponde a ninguna cuenta (code unauthenticated).
- 403 — la cuenta existe pero no está habilitada: pendiente de formalizar el acuerdo de servicio (code client_pending) o suspendida (code client_suspended). El cuerpo trae un message que lo explica y el code de §8.
- 429 — se superó el límite de peticiones por minuto.
3. Redes y activos
Estas son las redes y los stablecoins que se pueden usar en network y asset. Fíjate en los decimales: en BSC los contratos de USDT y USDC usan 18 decimales, en Ethereum y Polygon usan 6. Para pagar 1 USDT en BSC el amount es 1000000000000000000; en Ethereum es 1000000.
La misma tabla, con contratos, decimales y confirmaciones, está disponible por API sin autenticación en GET /v1/networks (§6.2): léela al arrancar en vez de copiarla.
| Red (network) | Chain ID | Activos (asset) y decimales | Gas nativo |
|---|---|---|---|
bsc |
56 |
USDT (18),
USDC (18)
|
BNB |
4. Crear un payout — POST /v1/payouts
Pide un pago desde tu wallet operativa a una dirección EVM. El pago queda en cola y lo ejecuta el motor de forma automática; el resultado te llega por webhook y también se puede consultar con GET.
Campos del cuerpo
| Campo | Tipo | Descripción |
|---|---|---|
external_reference |
string | Obligatorio. Tu identificador único del pago (hasta 191 caracteres). Es la clave de idempotencia y lo que vas a recibir de vuelta en el webhook. Los prefijos MPS-SW-, MPS-GF-, MPS-DP- y MPS-DC- están reservados (ver §7.4) y se rechazan con 422. |
network |
string | Obligatorio. Una de las redes de la tabla anterior (bsc, eth, pol). |
asset |
string | Obligatorio. USDT o USDC, soportado en esa red. |
amount |
string | Obligatorio. Entero positivo como string, en la unidad más chica del token (según los decimales de la red). Sin signo, sin punto decimal. |
destination_address |
string | Obligatorio. Dirección EVM del destinatario (0x + 40 hex). Si mezcla mayúsculas y minúsculas se valida el checksum EIP-55. |
metadata |
object | Opcional. Objeto JSON libre que se guarda tal cual y se devuelve en las respuestas de la API. No se incluye en los webhooks. Hasta 4 KB serializado y 3 niveles de anidamiento (422 si se pasa); lo mismo vale para la metadata de una dirección de depósito. |
Idempotencia
La combinación (tu cuenta, external_reference) es única. Si repites una petición con la misma referencia, no se crea otro pago ni se vuelve a contar contra tus límites: la API devuelve el payout que ya existe, con estado 200 en vez de 201, y el mismo cuerpo. Por eso, si una llamada se corta sin respuesta, lo correcto es reintentar con la misma referencia, nunca generar una nueva.
Los demás campos de la petición repetida se ignoran: lo que vale es lo que se guardó la primera vez.
Códigos de respuesta
- 201 — payout creado y en cola. El cuerpo es el objeto payout (§6).
- 200 — ya existía un payout con esa external_reference; el cuerpo es ese payout, en el estado en que esté.
- 422 — la petición no se aceptó: campo faltante o mal formado (cuerpo {message, errors}), activo no soportado en esa red, monto cero, dirección inválida o bloqueada, o límite de la cuenta superado (máximo por transacción o tope diario). No se creó nada. El cuerpo trae un code (§8) que dice si es final (validation, address_blocked, limit_exceeded.max_per_tx: reintentar sin cambiar la petición devuelve lo mismo) o solo cuestión de esperar (limit_exceeded.daily_cap: reintentar a partir de retry_at con la misma external_reference).
Ejemplo de petición
POST https://micalpays.sbs/api/v1/payouts
Authorization: Bearer mp_live_XXXXXXXXXXXXXXXXXXXXXXXX
Content-Type: application/json
Accept: application/json
{
"external_reference": "order-1234",
"network": "bsc",
"asset": "USDT",
"amount": "25000000000000000000",
"destination_address": "0x000000000000000000000000000000000000dEaD",
"metadata": {"order_id": 1234}
}
Ejemplo de respuesta (201)
{
"external_reference": "order-1234",
"network": "bsc",
"asset": "USDT",
"amount": "25000000000000000000",
"destination_address": "0x000000000000000000000000000000000000dEaD",
"status": "queued",
"tx_hash": null,
"explorer_url": null,
"error_message": null,
"metadata": {"order_id": 1234},
"created_at": "2026-09-17T18:30:00.000000Z",
"broadcast_at": null,
"completed_at": null,
"failed_at": null
}
5. Consultar un payout — GET /v1/payouts/{external_reference}
Devuelve el estado actual del payout identificado por tu referencia. Sirve para reconciliar: si por cualquier motivo no recibiste el webhook, consultando acá ves el mismo estado final que te habríamos avisado.
- 200 — el objeto payout (§6).
- 404 — no existe un payout con esa referencia en tu cuenta (code not_found). Los payouts de otras cuentas también responden 404, nunca 403.
6. El objeto payout
Es lo que devuelven POST y GET. Estos son todos los campos y no hay otros: cualquier campo adicional que aparezca en el futuro se sumará a esta lista antes de publicarse.
| Campo | Tipo | Descripción |
|---|---|---|
external_reference |
string | Tu referencia, tal como la enviaste. |
network |
string | Red del pago. |
asset |
string | Activo del pago. |
amount |
string | Monto como string entero en la unidad más chica del token. |
destination_address |
string | Dirección de destino. |
status |
string | Estado actual (ver tabla de estados). |
tx_hash |
string | null | Hash de la transacción en la cadena, o null hasta que se transmita. Puede estar presente en un payout todavía no completado (transacción transmitida y esperando confirmaciones). |
explorer_url |
string | null | Enlace al explorador público de la red para ese tx_hash, o null. |
error_message |
string | null | Motivo del fallo cuando status es failed; null en cualquier otro caso. |
metadata |
object | El objeto que enviaste al crearlo (o {} si no enviaste nada). |
created_at |
datetime | Cuándo se recibió la petición. |
broadcast_at |
datetime | null | Cuándo se transmitió la transacción a la red, o null. |
completed_at |
datetime | null | Cuándo se confirmó, o null. |
failed_at |
datetime | null | Cuándo se dio por fallido, o null. |
sponsored |
boolean | true si este pago necesitó que Micalpays le recargara gas nativo a tu wallet (ver §7.3); false si pagó con su propio gas. Informativo: desde la Fase 19 no cambia lo que se cobra. |
fee_amount |
string | null | La comisión de este pago (ver GET /v1/pricing: el multiplicador sobre el gas real de la transacción más el porcentaje de salida), como string en la unidad mínima de asset; null hasta que se complete. Es deuda liquidada a diario, nunca se descuenta del monto: el destinatario recibe amount entero. Es el mismo valor que llega en el webhook payout.completed. |
Estados
| Estado | ¿Final? | Descripción |
|---|---|---|
queued |
No | Aceptado y en cola; el motor lo toma en su próxima corrida (cada minuto). Un pago que acumula mucho hacia una dirección que tu cuenta nunca pagó (más de 2.000 en 24 horas) puede quedar un rato en queued mientras lo revisamos; el resto de tus pagos sigue normal. Si lo rechazamos, recibes payout.failed. |
processing |
No | El motor lo tiene tomado: firmando, transmitiendo o esperando confirmaciones. |
awaiting_float |
No | Tu wallet operativa no tiene saldo suficiente (stablecoin o gas) para pagarlo. Se reintenta solo en cuanto haya fondos; no hace falta volver a pedirlo. |
completed |
Sí | Confirmado en la cadena con las confirmaciones mínimas de la red (15 por defecto). tx_hash y completed_at están cargados. |
failed |
Sí | No se pudo pagar tras agotar los reintentos, o la transacción fue revertida. error_message explica por qué. No se vuelve a intentar: si corresponde, crea un payout nuevo con otra referencia. |
6.1 Tu wallet — GET /v1/wallet
Devuelve la wallet operativa activa de tu cuenta y su saldo por red, tal como lo ves en el portal: es la misma lectura, con el mismo caché de 60 segundos (updated_at dice de cuándo es). Sirve para mostrar en tu propio panel dónde recargar y cuánto hay, sin entrar al portal.
Además del saldo, cada red trae la necesidad calculada: lo que tus pagos en espera ya piden y falta (necesidad inmediata, exacta) y lo que tu operación reciente dice que va a pedir (cobertura en días y recarga sugerida). No hay mínimos que configurar: Micalpays no limita tus pagos, te informa cuánto falta y cuándo recargar.
| Campo | Tipo | Descripción |
|---|---|---|
address |
string | Dirección EVM de tu wallet operativa (la misma en todas las redes). Es a donde envías fondos para reponer float. |
balances |
object | Objeto con una entrada por red (bsc, eth, pol). Por cada red: cada activo en unidad mínima como string (USDT, USDC) y en unidades humanas con 8 decimales (USDT_human, USDC_human); el gas nativo de tu wallet (native, native_human, native_symbol); unsettled_gas_fees, la deuda de gas sin liquidar por asset (se genera solo si Micalpays tuvo que cubrirte gas alguna vez, se cobra al costo real sin margen y se liquida a diario); y los campos calculados: pending_outflow (USD-equivalente que tus pagos en queued/awaiting_float/processing ya piden), shortfall_stablecoin (lo que de eso, más la deuda de gas, no está en la wallet; 0 si no falta nada), avg_daily_outflow (salida diaria promedio de tus pagos completados, la mayor entre 7 y 30 días), avg_daily_inflow (entrada diaria promedio de 30 días por barridos de cobros hacia esta wallet, §6.4: la cobertura y la recarga sugerida se calculan sobre la salida neta), coverage_days (días de operación que cubre el saldo libre a ese ritmo neto; null sin histórico), suggested_topup (USD-equivalente a recargar para cubrir lo pendiente y varios días de operación, redondeado hacia arriba; 0 si no hace falta), avg_gas_per_payout y shortfall_gas (gas nativo promedio por pago según recibos reales y cuánto falta para los pendientes; solo informativo, si no alcanza lo cubre Micalpays) y payouts_payable (cuántos pagos alcanza a pagar tu gas propio). notification_preferences son tus preferencias de aviso (coverage_days, mute_projected, floors) — preferencias, no límites. float es un alias deprecado de floors + coverage_days y se retirará. Si no se pudo leer un saldo (nodo caído) ese valor y los que dependen de él vienen null, nunca un cero inventado. |
updated_at |
datetime | Cuándo se leyeron los saldos de la cadena (ISO-8601 UTC). Como mucho tienen 60 segundos. |
- 200 — el objeto wallet.
- 404 — tu cuenta todavía no tiene wallet operativa (code wallet_not_provisioned). Escríbenos.
Ejemplo de respuesta (200)
GET https://micalpays.sbs/api/v1/wallet
Authorization: Bearer mp_live_XXXXXXXXXXXXXXXXXXXXXXXX
Accept: application/json
{
"address": "0x1111111111111111111111111111111111111111",
"balances": {
"bsc": {
"USDT": "2500000000000000000000",
"USDT_human": "2500.00000000",
"USDC": "0",
"USDC_human": "0.00000000",
"native": "50000000000000000",
"native_human": "0.05000000",
"native_symbol": "BNB",
"unsettled_gas_fees": {"USDT": "0", "USDC": "0"},
"pending_outflow": "3100.00000000",
"shortfall_stablecoin": "600.00000000",
"shortfall_gas": "0.00000000",
"avg_daily_outflow": "850.00000000",
"avg_daily_inflow": "120.00000000",
"avg_gas_per_payout": "0.00004200",
"coverage_days": "0.00",
"payouts_payable": 1190,
"suggested_topup": "3200",
"notification_preferences": {"coverage_days": 2, "mute_projected": false, "floors": {"min": "0", "native_min": "0"}},
"float": {"min": "0", "native_min": "0", "coverage_days": 2}
},
"eth": {"USDT": null, "USDT_human": null, "USDC": "0", "USDC_human": "0.00000000", "native": "0", "native_human": "0.00000000", "native_symbol": "ETH", "unsettled_gas_fees": {"USDT": "0", "USDC": "0"}, "pending_outflow": "0.00000000", "shortfall_stablecoin": null, "shortfall_gas": null, "avg_daily_outflow": "0.00000000", "avg_gas_per_payout": null, "coverage_days": null, "payouts_payable": null, "suggested_topup": null, "notification_preferences": {"coverage_days": 2, "mute_projected": false, "floors": {"min": "0", "native_min": "0"}}, "float": {"min": "0", "native_min": "0", "coverage_days": 2}},
"pol": {"USDT": "0", "USDT_human": "0.00000000", "USDC": "0", "USDC_human": "0.00000000", "native": "0", "native_human": "0.00000000", "native_symbol": "POL", "unsettled_gas_fees": {"USDT": "0", "USDC": "0"}, "pending_outflow": "0.00000000", "shortfall_stablecoin": "0.00000000", "shortfall_gas": "0.00000000", "avg_daily_outflow": "0.00000000", "avg_gas_per_payout": "0.00004200", "coverage_days": null, "payouts_payable": 0, "suggested_topup": "0", "notification_preferences": {"coverage_days": 2, "mute_projected": false, "floors": {"min": "0", "native_min": "0"}}, "float": {"min": "0", "native_min": "0", "coverage_days": 2}}
},
"updated_at": "2026-09-17T18:30:00.000000Z"
}
No hay ninguna configuración que active o desactive esto: si tu wallet mantiene BNB/POL/ETH propio, Micalpays nunca le toca el gas y unsettled_gas_fees se queda en 0; si en algún momento no alcanza, Micalpays lo cubre y ahí aparece la deuda.
6.2 Cobros: direcciones de depósito — /v1/deposit-addresses
Además de pagar, Micalpays cobra por cuenta tuya: le pides una dirección de depósito por cada cosa que quieras cobrar (un pedido, un usuario, una factura), se la muestras al pagador y Micalpays vigila la cadena, te avisa por webhook cuando el dinero llega y se confirma, y barre el saldo al destino que elijas — por defecto tu Safe si tienes uno verificado en esa red, si no tu wallet operativa, donde queda como float disponible para tus payouts sin reposición manual ("cobros que financian tus pagos").
Una dirección es una dirección EVM propia (con su clave, custodiada por nosotros) válida a la vez en las tres redes: la muestras una sola vez y el pagador elige la red y el asset (USDT o USDC). Se identifica por un label tuyo, que es la clave de idempotencia: pedir dos veces el mismo label devuelve la misma dirección mientras esté activa. Hay un tope de direcciones activas por cuenta (500); desactivar libera cupo, y una dirección sin depósitos durante 120 días se desactiva sola. Lo que llegue a una dirección desactivada se registra y se avisa igual: nunca se pierde plata.
Endpoints
POST /v1/deposit-addresses— crea (201) o devuelve (200) la dirección activa del label. Cuerpo: label (obligatorio, hasta 191 caracteres), single_use (boolean: se desactiva sola al barrer), metadata (objeto libre que vuelve en cada webhook deposit.*), expected_amount (unidades humanas, USD-equivalente: "25.50"), expected_asset (USDT o USDC) y expires_at (ISO-8601). 429 deposit_address_cap_reached si llegaste al tope.GET /v1/deposit-addresses/{reference}— el objeto deposit_address. 404 si la referencia no es de tu cuenta.PATCH /v1/deposit-addresses/{reference}— cambia status (active / inactive), single_use, metadata, expected_amount, expected_asset y expires_at. Reactivar cuenta contra el tope y falla con 409 deposit_address_label_in_use si otra dirección activa ya lleva ese label. El label no se edita.GET /v1/deposit-addresses/{reference}/balances— saldo vivo de la dirección por red y asset (unidad mínima y _human), con caché de 60 segundos y un límite propio de 10 peticiones por minuto. Para saber cuánto entró usa los webhooks o GET /v1/deposits: el saldo cambia cuando barremos.POST /v1/deposit-addresses/{reference}/sweep— barre ahora el saldo confirmado de esa dirección, sin esperar el umbral mínimo (es la única forma de barrer con sweep_mode = manual), con un límite propio de 3 pedidos por minuto por dirección. 202 con la lista sweeps y, en skipped, por qué no salió cada red/asset: sweep_in_flight, empty_balance, destination_unavailable, balance_unavailable, gas_unavailable o uneconomic (el neto no cubriría al menos el gas de moverlo: no se firma nada).GET /v1/networks— catálogo público de redes (sin API Key): chain id, símbolo del gas, contratos y decimales de cada asset, confirmaciones con las que se completa un payout y se confirma un depósito, URL del explorador. Léelo al arrancar en vez de copiar la tabla de §3.
El objeto deposit_address
| Campo | Tipo | Descripción |
|---|---|---|
reference |
string | Referencia pública opaca (MPD-…), la única que se expone. Es la que usas en los demás endpoints. |
address |
string | La dirección EVM (0x + 40 hex, minúsculas), la misma en todas las redes de networks. |
networks |
string[] | Redes en las que la dirección recibe: hoy bsc, eth y pol. |
label |
string | Tu label, tal como lo enviaste. |
status |
string | active o inactive. |
single_use |
boolean | Si se desactiva sola al completarse el primer barrido. |
metadata |
object | El objeto que enviaste (o {}). Viaja en cada webhook deposit.* de esa dirección. |
expected_amount |
string | null | Monto esperado en unidades humanas, o null. Sirve para amount_status en los depósitos. |
expected_asset |
string | null | USDT, USDC o null (cualquiera). |
expires_at |
datetime | null | Vencimiento opcional. Un depósito posterior se registra igual, con deposit_address_status "expired" en el webhook. |
last_deposit_at |
datetime | null | Cuándo se detectó el último depósito, o null. |
deactivated_at |
datetime | null | Cuándo se desactivó, o null. |
created_at |
datetime | Cuándo se creó. |
Ejemplo de petición
POST https://micalpays.sbs/api/v1/deposit-addresses
Authorization: Bearer mp_live_XXXXXXXXXXXXXXXXXXXXXXXX
Content-Type: application/json
Accept: application/json
{
"label": "order-1234",
"expected_amount": "25.50",
"expected_asset": "USDT",
"metadata": {"order_id": 1234}
}
Ejemplo de respuesta (201)
{
"reference": "MPD-VM5V1C7RZ94JNRD158JK",
"address": "0x8d1e5b3c9a7f2e4d6c8b0a1f3e5d7c9b1a3f5e7d",
"networks": ["bsc", "eth", "pol"],
"label": "order-1234",
"status": "active",
"single_use": false,
"metadata": {"order_id": 1234},
"expected_amount": "25.5",
"expected_asset": "USDT",
"expires_at": null,
"last_deposit_at": null,
"deactivated_at": null,
"created_at": "2026-09-23T12:00:00.000000Z"
}
6.3 El objeto deposit — GET /v1/deposits
Un deposit es una transferencia de USDT/USDC detectada en una de tus direcciones: un evento Transfer de la cadena, único por red + tx_hash + log_index (una misma transacción puede traer dos transferencias). Las transferencias de valor cero (address poisoning) y las de menos de 0,50 USD (polvo) no se registran.
Contrato de montos: el de toda la API (amount en la unidad mínima como string + amount_human) más amount_usd y price_usd, congelados al detectar (1,00 para stablecoin), para comparar contra el total de un pedido sin cambiar de convención. amount_status compara amount_human con el expected_amount de la dirección: matches (con 1 % de tolerancia por debajo, por las comisiones del exchange del pagador), underpaid, overpaid o none (sin monto esperado, o el asset esperado es otro).
GET /v1/deposits— historial, más nuevo primero, paginado por cursor: limit (1–100, por defecto 50) y cursor (el next_cursor de la respuesta anterior; null cuando no hay más). Filtros: deposit_address (referencia o dirección), status, asset, network, from y to (por detected_at, ISO-8601). Respuesta: {data: [deposit…], next_cursor}.GET /v1/deposits/{reference}— un depósito por su referencia (MPD-TX-…). 404 si no es de tu cuenta.POST /v1/deposits/check-hash— conciliación manual (5 peticiones por minuto): {network, tx_hash}. Importa cada transferencia de esa transacción que pague a una dirección tuya —aunque el escáner ya la haya dejado atrás o la dirección esté inactiva (la reactiva si hay cupo)— y responde found, verified (false si los nodos independientes todavía no coinciden sobre ese recibo: no se importó nada, reintenta en un minuto), credited (true si importó algo nuevo), matches, new_transactions, duplicates, reactivated (referencias de direcciones reactivadas) y deposits. Un duplicado nunca crea nada; la confirmación y el aviso siguen su curso normal.
| Campo | Tipo | Descripción |
|---|---|---|
reference |
string | Referencia pública del depósito (MPD-TX-…). |
deposit_address |
string | La dirección que recibió. |
deposit_address_reference |
string | La referencia (MPD-…) de esa dirección. |
label |
string | El label de la dirección: es lo que enlaza el cobro con tu pedido o tu usuario. |
metadata |
object | La metadata de la dirección. |
deposit_address_status |
string | active, inactive o expired en el momento del aviso, para que decidas qué hacer con un cobro tardío. |
network |
string | Red en la que llegó. |
asset |
string | USDT o USDC. |
amount |
string | Monto como string entero en la unidad mínima del token (según los decimales de la red). |
amount_human |
string | El mismo monto en unidades humanas, 8 decimales. |
amount_usd |
string | null | USD-equivalente al detectar (amount_human × price_usd), o null si aún no se cotizó. |
price_usd |
string | null | Cotización congelada al detectar (1 para stablecoin). |
from_address |
string | Dirección que pagó. |
tx_hash |
string | Hash de la transacción. |
explorer_url |
string | null | Enlace al explorador de la red. |
block_number |
integer | Bloque en el que se minó (puede cambiar por un reorg mientras no esté confirmado). |
confirmations |
integer | Confirmaciones al momento del aviso o de la consulta. |
min_confirmations |
integer | Las que necesita para confirmarse: el default de la red (bsc 10, eth 10, pol 20) o deposit_settings.min_confirmations de tu cuenta. |
status |
string | Estado actual (ver tabla). |
expected_amount |
string | null | El expected_amount de la dirección, o null. |
amount_status |
string | matches, underpaid, overpaid o none. |
detected_at |
datetime | Cuándo lo detectó Micalpays. |
confirmed_at |
datetime | null | Cuándo alcanzó las confirmaciones, o null. |
held_reason |
string | null | from_address_blocked o rejected_by_admin cuando está retenido o rechazado; null en otro caso. |
sweep_reference |
string | null | Referencia del barrido (MPS-DP-…) que lo movió, o null. |
sweep_tx_hash |
string | null | Hash de la transacción del barrido, o null. |
swept_at |
datetime | null | Cuándo se barrió, o null. |
Estados
| Estado | ¿Final? | Descripción |
|---|---|---|
detected |
No | Visto en un bloque; esperando confirmaciones. No acredites todavía. |
confirmed |
No | Alcanzó las confirmaciones: es el momento de acreditar (evento deposit.confirmed). |
notified |
No | Confirmado y avisado a tu webhook. A efectos tuyos es lo mismo que confirmed. |
swept |
Sí | Su saldo salió de la dirección hacia tu destino (evento deposit.swept). |
held |
No | Retenido por cumplimiento: el origen figura en la lista de direcciones bloqueadas (Términos §5). No se barre hasta que nuestro equipo lo revise; recibiste deposit.held. |
rejected |
Sí | Rechazado tras la revisión: no se barre nunca. Estado final. |
Ejemplo: webhook deposit.confirmed
POST {your webhook_url}
Content-Type: application/json
X-Signature-256: sha256=9b41d2…07ac
{
"event": "deposit.confirmed",
"timestamp": "2026-09-23T12:04:12.000000Z",
"data": {
"reference": "MPD-TX-4Q7ZK2M9RCX1VB8T6NHW",
"deposit_address": "0x8d1e5b3c9a7f2e4d6c8b0a1f3e5d7c9b1a3f5e7d",
"deposit_address_reference": "MPD-VM5V1C7RZ94JNRD158JK",
"label": "order-1234",
"metadata": {"order_id": 1234},
"deposit_address_status": "active",
"network": "bsc",
"asset": "USDT",
"amount": "25500000000000000000",
"amount_human": "25.50000000",
"amount_usd": "25.5",
"price_usd": "1",
"from_address": "0x3f5e7d8d1e5b3c9a7f2e4d6c8b0a1f3e5d7c9b1a",
"tx_hash": "0x7c2e…91fd",
"explorer_url": "https://bscscan.com/tx/0x7c2e…91fd",
"block_number": 61234567,
"confirmations": 10,
"min_confirmations": 10,
"status": "confirmed",
"expected_amount": "25.5",
"amount_status": "matches",
"detected_at": "2026-09-23T12:03:40.000000Z",
"confirmed_at": "2026-09-23T12:04:11.000000Z"
}
}
6.4 Barridos y política de barrido
Cada 5 minutos Micalpays mueve el saldo confirmado de cada dirección (por red y asset) a tu destino en una sola transacción, que ejecuta el mismo motor de tus payouts: la referencia es interna y empieza con MPS-DP-. Se barre la suma de los depósitos confirmados, nunca un depósito retenido ni uno todavía sin confirmar. Hay un umbral mínimo por red para no gastar gas en migajas (por defecto 1 USD en BSC y Polygon, 50 USD en Ethereum), que el barrido manual ignora.
Costo: se descuenta del propio cobro, nunca queda como deuda. Antes de barrer, el tanque de Micalpays le carga a la dirección el gas de sus dos transferencias; la comisión es el porcentaje vigente sobre el bruto (§9, GET /v1/pricing) más ese gas al costo real (lo que el tanque transfirió y el gas de esa recarga, convertido al asset). Tu destino recibe el neto (bruto − comisión) y la comisión sale de la misma dirección hacia el tanque, en una transferencia aparte, después del neto. Si la dirección ya tenía gas sobrante de un barrido anterior, ese gas no se vuelve a cobrar. En una dirección single_use, el gas que sobra vuelve al tanque al desactivarse.
Tu política vive en deposit_settings (portal → Cobros, o PATCH /v1/account): sweep_destination auto (Safe verificado en esa red, si no wallet operativa), operational_wallet, safe (solo donde esté verificado) o external (tu destination_wallet_address, con el mismo bloqueo de 48 horas al cambiarla que el retiro del saldo restante); sweep_mode auto o manual; min_sweep_usd por red; min_confirmations; notify_confirmation_updates.
| Campo | Descripción |
|---|---|
reference |
Referencia del barrido, igual a la del payout interno (MPS-DP-…). |
gross_amount |
Lo que salió de la dirección (unidad mínima; gross_amount_human en unidades humanas). |
fee_amount |
La comisión porcentual de payin descontada del bruto (fee_amount_human en unidades humanas). |
gas_amount |
El gas del barrido al costo real, descontado del bruto (gas_amount_human). |
net_amount |
gross_amount − fee_amount − gas_amount: lo que llega a tu destino (net_amount_human). |
destination |
Dirección a la que fue. |
destination_type |
operational_wallet, safe o external. |
sweep_tx_hash |
Hash de la transacción del barrido. |
funding_tx_hash |
Hash de la recarga de gas del tanque que lo pagó, o null si la dirección tenía gas propio. |
sponsored |
true si hizo falta esa recarga. |
source_tx_hashes |
Hashes de los depósitos que incluye. |
deposits |
Lista de {reference, tx_hash, amount, amount_human} de los depósitos barridos. |
6.5 Tu cuenta — GET /v1/account y PATCH /v1/account
Lo que tu cuenta sabe de sí misma y puede ajustar sin entrar al portal. GET devuelve name, business_id, status, locale, capabilities, limits, webhook_url, webhook_secret_hint (inicio…final del secreto, nunca entero), min_webhook_confirmations, destination_wallet_address, sweep_locked_until, operational_wallet_address, safe_address, deposit_settings (min_confirmations, sweep_destination, sweep_mode, min_sweep_usd por red, notify_confirmation_updates, webhook_dialect), notification_preferences, terms_version y created_at.
PATCH acepta cualquier subconjunto de webhook_url, webhook_secret, destination_wallet_address, min_webhook_confirmations, notify_confirmation_updates y deposit_settings; lo que no envías se conserva, y deposit_settings se mezcla clave a clave. Cambiar destination_wallet_address bloquea durante 48 horas el retiro del saldo restante y el barrido external, y avisa del cambio a tu correo operativo; durante esas 48 horas puedes volver al destino anterior desde el portal. Si tu cuenta tiene segundo factor activo, cambiar webhook_url, webhook_secret o destination_wallet_address exige además two_factor_code (el código de 6 dígitos de tu app): sin él responde two_factor_required. Cada cambio se avisa con account.settings_changed al webhook anterior.
PATCH https://micalpays.sbs/api/v1/account
Authorization: Bearer mp_live_XXXXXXXXXXXXXXXXXXXXXXXX
Content-Type: application/json
{
"notify_confirmation_updates": true,
"deposit_settings": {
"sweep_destination": "auto",
"sweep_mode": "auto",
"min_confirmations": 12,
"min_sweep_usd": {"eth": "100"}
}
}
6.6 Si vienes de otra pasarela de cobros
Si ya cobras en cripto con una pasarela de direcciones por label (ciclo detectado → confirmado → barrido, webhooks firmados con X-Signature-256: sha256=<hex> sobre el cuerpo crudo), el modelo es el mismo y la migración es cambiar la URL base, la API Key y unos nombres. La tabla usa como referencia los nombres más habituales en ese tipo de integraciones:
Si no quieres tocar tu receptor todavía: con deposit_settings.webhook_dialect = transaction (portal → Cobros, o PATCH /v1/account) los mismos avisos salen como transaction.incoming, transaction.confirmation_update, transaction.confirmed y transaction.swept, con el data que ya lees (amount en USD-equivalente, amount_native, wallet_label, to_address, block_number) más nuestros campos extra; deposit.held no tiene equivalente y no se traduce. Cambias la URL de webhook y la llave, y listo.
| Tu integración actual | Micalpays |
|---|---|
POST /wallets/assign {label, single_use} |
POST /v1/deposit-addresses {label, single_use, metadata, expected_amount, expires_at}; label se conserva tal cual. |
GET /wallets/balance |
GET /v1/deposit-addresses/{reference}/balances (10 por minuto, con caché). |
GET /wallets/transactions |
GET /v1/deposits (cursor, filtros). |
POST /wallets/check-hash |
POST /v1/deposits/check-hash {network, tx_hash}. |
GET /networks |
GET /v1/networks (público). |
GET/PATCH /clients/me |
GET/PATCH /v1/account. |
transaction.incoming |
deposit.detected. |
transaction.confirmed |
deposit.confirmed (el evento para acreditar). |
transaction.swept |
deposit.swept — con reintentos, y con la lista de depósitos que incluye. |
transaction.confirmation_update |
deposit.confirmation_update (opcional, notify_confirmation_updates). |
amount en USD, amount_native |
amount (unidad mínima, string) + amount_human + amount_usd + price_usd: el USD-equivalente sigue estando, pero el monto principal es el del token, como en el resto de la API. |
wallet_label |
label (y deposit_address_reference). |
Sin equivalente |
deposit.held (cumplimiento), expected_amount y amount_status, metadata por dirección, destino del barrido configurable (Safe o wallet operativa: lo cobrado financia tus payouts) y gas al costo en vez de un porcentaje. |
6.7 Estado de cuenta — GET /v1/statement
Todo lo que necesitas para cuadrar tu saldo interno contra lo que Micalpays custodia por ti, en una sola lectura: el saldo en vivo de tu wallet operativa y de tu Safe (mismos cachés de 60 segundos que el portal), lo depositado que todavía no se barrió, y el historial de depósitos, pagos, comisiones, cargas recibidas en tu wallet y reposiciones desde el Safe.
Montos en unidades humanas con 8 decimales, como string. Cada lista viene ordenada de la más nueva a la más vieja y trae como mucho 1000 filas; truncated en true avisa que alguna quedó cortada.
| Campo | Tipo | Descripción |
|---|---|---|
custody |
object | operational_wallet (address, balances por red con cada activo, native y native_symbol, updated_at), safe (address y balances por red; null si no registraste Safe) y pending_sweep (por red y activo, lo cobrado que todavía no se barrió). Un saldo que no se pudo leer viene null, nunca un cero inventado. |
deposits |
array | Cada cobro: reference, network, asset, amount, from_address, tx_hash, status, detected_at y, si ya se barrió, sweep_reference, sweep_fee, sweep_gas y swept_at. |
payouts |
array | Cada pago: reference, kind (client para los tuyos; deposit_sweep, deposit_commission, gas_fee_settlement y sweep para los internos que también mueven tus fondos), network, asset, amount, status, destination_address, tx_hash, created_at y completed_at. |
fee_charges |
array | Cada comisión cobrada: network, asset, amount, source (inbound_fee o gas), payout_reference (el pago o barrido que la generó), settled_at y settlement_reference. |
wallet_receipts |
array | Cada carga recibida en tu wallet operativa: network, asset, amount, from_address, from_safe (true si vino de tu Safe), tx_hash y confirmed_at. |
float_replenishments |
array | Cada reposición desde tu Safe: network, asset, amount, status, tx_hash y executed_at. |
truncated |
boolean | true si alguna lista superó las 1000 filas. |
generated_at |
datetime | Cuándo se armó la respuesta. |
- 200 — el estado de cuenta.
- 401 — API Key ausente o inválida.
7. Webhooks
Cuando un payout llega a un estado final (y, una sola vez, cuando queda esperando float), cuando tu wallet necesita fondos, cuando llega una carga y en cada paso de un cobro (deposit.*), hacemos un POST a la URL de webhook configurada en tu cuenta (portal → Configuración, o PATCH /v1/account). Si no configuras una URL no se envía nada y puedes reconciliar por GET. Desde el portal, en la pestaña Probador de webhooks, puedes enviarte un evento de prueba con exactamente este formato y esta firma.
7.1 Sobre (envelope)
Todo webhook tiene la misma forma: event (nombre del evento), timestamp (cuándo se generó, ISO-8601 UTC) y data (el contenido, que depende del evento).
POST {your webhook_url}
Content-Type: application/json
X-Signature-256: sha256=3f7a0c…e91b
{
"event": "payout.completed",
"timestamp": "2026-09-17T18:32:10.000000Z",
"data": {
"external_reference": "order-1234",
"network": "bsc",
"asset": "USDT",
"amount": "25000000000000000000",
"destination_address": "0x000000000000000000000000000000000000dEaD",
"status": "completed",
"tx_hash": "0x9c1f…4d2a",
"error_message": null,
"completed_at": "2026-09-17T18:32:05.000000Z",
"failed_at": null,
"sponsored": true,
"fee_amount": "750000000000000000"
}
}
sponsored (boolean) y fee_amount (string, unidad mínima de asset) llegan en payout.completed: sponsored es true cuando ese retiro puntual necesitó que Micalpays le recargara gas a tu wallet (solo informativo), y fee_amount es la comisión del pago — el multiplicador vigente sobre el gas real que costó la transacción más el porcentaje de salida (GET /v1/pricing) — ya cargada como deuda, liquidada a diario. El destinatario siempre recibió amount completo, este cargo nunca sale de ahí.
7.2 Firma
Cada entrega lleva el header X-Signature-256 con el valor sha256=<hex>, donde <hex> es el HMAC-SHA256 en hexadecimal minúscula del cuerpo crudo de la petición (los bytes exactos que recibes, sin re-serializar) usando como clave el secreto de webhook de tu cuenta.
Para verificarla: lee el cuerpo crudo, calcula el HMAC con tu secreto, y compara con el header usando una comparación de tiempo constante. Rechaza (con cualquier código que no sea 2xx, o simplemente ignorando) cualquier petición cuya firma no coincida. Solo ese header y ese secreto autentican la petición: no confíes en la IP de origen.
Ejemplo de verificación (PHP)
$raw = file_get_contents('php://input');
$expected = 'sha256='.hash_hmac('sha256', $raw, $webhookSecret);
if (! hash_equals($expected, $_SERVER['HTTP_X_SIGNATURE_256'] ?? '')) {
http_response_code(401);
exit;
}
$payload = json_decode($raw, true);
// ... handle $payload['event'] / $payload['data'] ...
http_response_code(200);
7.3 Eventos
| Evento | Cuándo se envía | Contenido de data |
|---|---|---|
payout.completed |
El payout se confirmó en la cadena. | external_reference, network, asset, amount, destination_address, status ("completed"), tx_hash, error_message (null), completed_at, failed_at (null). |
payout.failed |
El payout se dio por fallido. | Los mismos campos, con status "failed", error_message con el motivo, failed_at, y tx_hash presente solo si llegó a transmitirse. |
payout.awaiting_float |
El payout quedó esperando float por primera vez: tu wallet operativa no tiene saldo suficiente (stablecoin o gas) para pagarlo. Se envía una sola vez por payout, aunque siga esperando días; no es un estado final, y el payout.completed o payout.failed posterior llega igual. Sirve para avisarle a tu equipo que hay que recargar. En el mismo minuto llega también un wallet.low_balance con el monto exacto que falta. | Los mismos campos, con status "awaiting_float", tx_hash null, error_message null, completed_at y failed_at null. |
wallet.low_balance |
Tu wallet operativa necesita fondos en una red: reason "immediate" cuando hay pagos esperando más de lo que hay (con el monto exacto), "projected" cuando la cobertura calculada con tu operación reciente cae bajo el umbral, "gas" cuando el gas nativo no alcanza y no hay tanque que lo cubra, "floor" cuando el saldo baja de un piso opcional que tú pediste. No se repite mientras la situación siga igual: vuelve a enviarse si empeora o cada 24 h, y se rearma cuando se resuelve. Sin mínimos configurados también sale: se calcula a partir de tu cola y tu histórico. | network, wallet_address, asset_balances (por asset, unidades humanas), native_balance, native_symbol, unsettled_gas_fees, pending_outflow, shortfall_stablecoin, shortfall_gas, avg_daily_outflow, avg_daily_inflow, avg_gas_per_payout, coverage_days, payouts_payable, suggested_topup y reason — los mismos campos y unidades que GET /v1/wallet (§6.1). |
funds.received |
Llegó una carga de USDT/USDC a tu wallet operativa y alcanzó las confirmaciones que pediste (min_webhook_confirmations). Se envía una sola vez por transferencia, y no antes de llegar a ese número de confirmaciones. Las transferencias de valor cero no cuentan. Sirve para saber que la recarga llegó, con su monto y su hash. Los barridos de tus cobros hacia la wallet operativa (§6.4) también son una carga y disparan funds.received, con from_address igual a la dirección de depósito barrida: si ya acreditaste ese dinero con deposit.confirmed, no lo vuelvas a acreditar. | network, asset, amount (string entero en la unidad más chica del token, como en los payouts), amount_human (unidades humanas), from_address, wallet_address, tx_hash, explorer_url, block_number, confirmations y detected_at (cuándo la detectó Micalpays). |
deposit.detected |
Se detectó un cobro en una de tus direcciones de depósito (todavía sin las confirmaciones). Informativo: no acredites con este evento. | El objeto deposit de §6.3 (reference, deposit_address, deposit_address_reference, label, metadata, deposit_address_status, network, asset, amount, amount_human, amount_usd, price_usd, from_address, tx_hash, explorer_url, block_number, confirmations, min_confirmations, status "detected", expected_amount, amount_status, detected_at, confirmed_at null). |
deposit.confirmed |
El cobro alcanzó las confirmaciones (min_confirmations; un cobro de 1.000 USD o más exige el triple, para acotar el riesgo de un reorg profundo). Es el evento para acreditar: una sola vez por depósito. | Los mismos campos, con status "confirmed", confirmations y confirmed_at. |
account.api_key_rotated |
Se rotó la API Key de tu cuenta (portal, admin o recuperación). Un intento, informativo. | reason (portal | admin | recovery), new_api_key_prefix, previous_valid_until, business_id, occurred_at. Nunca viaja la llave. |
account.two_factor_changed |
Se activó o desactivó el segundo factor de tu cuenta desde el portal. | enabled (boolean), business_id, occurred_at. |
account.two_factor_reset |
Se reseteó el 2FA con un código de recuperación (dispositivo perdido) y la cuenta entró en enfriamiento. | cooldown_until, business_id, occurred_at. |
account.recovered |
Alguien recuperó una credencial de tu cuenta con el 2FA. Si no fuiste tú, rota la API Key y escríbenos. | lost (api_key | business_id), method (totp | recovery_code), cooldown_until, business_id, occurred_at. |
account.settings_changed |
Cambió el webhook, su secreto o la wallet de destino (portal, API o "volver al destino anterior"). Se envía al webhook ANTERIOR, firmado con el secreto anterior. Un intento, informativo. | fields (lista de lo que cambió), source (portal | api | portal_revert), destination_wallet_address {from, to} y sweep_locked_until si cambió el destino, webhook_url {from, to} si cambió el webhook, business_id, occurred_at. Nunca viaja el secreto. |
account.payouts_paused |
Se pausaron los payouts de tu cuenta por gasto anómalo (muchas direcciones nuevas en pocos minutos). Un intento, informativo. | reason, business_id, occurred_at. |
account.login_failures |
Tu cuenta quedó bloqueada un rato por intentos fallidos de ingreso, segundo factor o recuperación desde un navegador que no es uno tuyo reconocido. Nadie entró; es para que sepas que la están golpeando (como mucho uno por hora). Desde un navegador donde ya entraste antes puedes seguir ingresando, y la API no se ve afectada. | channel (login | two_factor | recovery), ip, business_id, occurred_at. |
account.pricing_changed |
Un cambio de precio quedó programado (o se aplicó) desde nuestro lado. Un solo intento, informativo. | changes (payin.percentage, payin.minimum_usd, payout.percentage, payout.gas_multiplier: {from, to}), effective_at, pricing (el mismo objeto de GET /v1/pricing para tu cuenta) — ver §9. |
deposit.swept |
El saldo confirmado de la dirección salió hacia tu destino. Uno por barrido (puede incluir varios depósitos), con reintentos como los demás. | reference (MPS-DP-…), deposit_address, deposit_address_reference, label, metadata, network, asset, gross_amount, gross_amount_human, fee_amount, fee_amount_human, gas_amount, gas_amount_human, net_amount, net_amount_human, destination, destination_type, sweep_tx_hash, explorer_url, funding_tx_hash, sponsored, source_tx_hashes, deposits (lista), status "completed", swept_at — ver §6.4. |
deposit.held |
El cobro quedó retenido por cumplimiento: el origen figura en la lista de direcciones bloqueadas. No se barre hasta que nuestro equipo lo revise (y entonces sigue como un depósito normal, o queda rechazado). Se envía en vez de deposit.detected. | Los mismos campos del deposit, con status "held". |
deposit.confirmation_update |
Solo si activas notify_confirmation_updates: cada vez que cambia el número de confirmaciones de un depósito todavía sin confirmar. Un solo intento, sin reintentos; nunca antes de deposit.detected. | Los mismos campos del deposit, con status "detected" y confirmations actualizado. |
Vamos a sumar más eventos con el tiempo; se anunciarán acá antes de activarse.
Los eventos de prueba enviados desde el portal usan el nombre de evento que elijas (payout.* o deposit.*), llevan data.test = true y una referencia (external_reference en los payouts, reference en los cobros) que empieza con MPS-TEST-. Trátalos como lo que son: no corresponden a ningún payout ni a ningún cobro, y no acredites nada por ellos.
7.4 Reglas para tu receptor
- Responde 200 a todo lo que verifique la firma, incluidos los eventos que no conoces. Vamos a agregar eventos nuevos con el tiempo (los reservados de arriba y otros). Un evento desconocido no es un error: verifica la firma, responde 2xx y descártalo. Si respondes otra cosa, lo reintentamos como si hubiera fallado y terminamos alertando a nuestro equipo por nada.
- Referencias con prefijo MPS-SW-, MPS-GF-, MPS-DP- y MPS-DC-. Cuando retiras desde el portal el saldo restante de tu wallet operativa a tu wallet de destino (el envío se encola en el acto, con el costo del gas descontado del monto), Micalpays lo ejecuta como un payout interno cuya external_reference empieza con MPS-SW- (por ejemplo MPS-SW-VM5V1C7RZ94JNRD158JK). Llega por este mismo webhook con payout.completed o payout.failed. No lo pediste por API, así que no va a coincidir con ninguna de tus referencias: responde 200 y regístralo o ignóralo, pero no lo trates como error. Lo mismo pasa con el prefijo MPS-GF-: es la liquidación diaria de la deuda de gas que Micalpays te cubrió (unsettled_gas_fees en §6.1), un payout interno de tu wallet hacia Micalpays por el costo real, que también llega por este webhook. El prefijo MPS-DP- es el barrido de una dirección de depósito (§6.4): ese no llega como payout.*, sino como deposit.swept, pero puede aparecer si consultas tu historial. Lo mismo el prefijo MPS-DC-: la comisión de ese cobro, que sale de la dirección de depósito hacia el tanque de gas de Micalpays. Los cuatro prefijos están reservados y no puedes usarlos en tus propias referencias.
- Puede llegar más de una vez. Si tu endpoint respondió con error o la respuesta se perdió, reintentamos la misma entrega; el reenvío manual desde nuestro panel también puede repetirla. Haz que el procesamiento sea idempotente por external_reference + event: un payout.completed que ya aplicaste se vuelve a responder con 200 sin volver a acreditar nada. Para los cobros, la clave es reference + event (o tx_hash + log_index, que es lo que hace único a un depósito).
- No asumas orden ni exclusividad. Los eventos de distintos payouts o cobros pueden llegar en cualquier orden. Un mismo payout solo llega a un estado final, así que nunca vas a recibir completed y failed para la misma referencia; sí puedes recibir antes un payout.awaiting_float (a lo sumo uno) para esa misma referencia. En los cobros, un reintento puede hacer que deposit.detected llegue después de deposit.confirmed: decide por status y no por el orden de llegada, y acredita solo con deposit.confirmed (una sola vez por reference).
- Responde rápido. Esperamos hasta 10 segundos. Si tu procesamiento tarda más, encola el trabajo y responde 200 enseguida; un timeout cuenta como fallo y se reintenta.
- Reacciona a los eventos, no a la API. No hace falta llamar a GET al recibir el webhook: data trae todo lo necesario. Usa la API para reconciliar lo que no haya llegado: GET /v1/payouts/{external_reference} para tus pagos pendientes de hace más de una hora, y GET /v1/deposits?status=confirmed (o POST /v1/deposits/check-hash con el hash que te pase tu usuario) para los cobros.
7.5 Reintentos
Una entrega se da por exitosa cuando tu endpoint responde 2xx. Cualquier otra respuesta, un timeout (10 s) o un error de conexión la marcan como fallida y se reintenta con backoff. Son 3 intentos en total:
| Intento | Espera desde el anterior |
|---|---|
| 1 | inmediato, al generarse el evento |
| 2 | 30 segundos |
| 3 | 5 minutos |
Si los 3 intentos fallan, dejamos de reintentar automáticamente y nuestro equipo recibe una alerta; podemos reenviar la entrega a mano una vez que tu endpoint vuelva a responder. Mientras tanto el estado del payout o del cobro es el que ves con la API (GET /v1/payouts/…, GET /v1/deposits), así que ningún pago ni cobro se pierde por un webhook no entregado.
8. Formato de errores
Todos los errores devuelven JSON con un message legible (para humanos: su texto puede cambiar, no lo compares) y un code estable (para tu programa: en inglés, con puntos como separador, de la tabla de abajo). Los errores de validación (422) agregan errors, un objeto con el nombre de cada campo y la lista de mensajes de ese campo. Solo limit_exceeded.daily_cap agrega retry_at (ISO-8601 UTC): el instante en que se libera el cupo.
Códigos
| code | HTTP | Cuándo aparece | Qué hacer |
|---|---|---|---|
unauthenticated |
401 | Falta la API Key o no corresponde a ninguna cuenta. | Final. Revisa la llave (¿la rotaste?). No reintentes hasta corregirla. |
client_pending |
403 | La cuenta existe pero todavía no está habilitada. | Final hasta que un humano la habilite. Alerta a tu equipo; no reintentes en bucle. |
client_suspended |
403 | La cuenta está suspendida. | Final. Alerta a tu equipo y escríbenos. |
client_inactive |
403 | La cuenta está en un estado que no opera (ni pendiente ni suspendida). | Final. Escríbenos. |
sandbox_only |
403 | POST /v1/sandbox/* con una API Key real (mica_…): los simuladores solo existen en el sandbox (§10). | Usa tu API Key de prueba (mica_test_…) para simular; en producción los cobros y pagos son reales. |
payouts_paused |
403 | POST /v1/payouts con los payouts de tu cuenta en pausa por revisión de seguridad: demasiados pagos a direcciones nuevas en pocos minutos (freno automático) o una pausa de un administrador. Los ya encolados tampoco se firman mientras dure. | Escríbenos: la reanudamos tras revisar. Si no reconoces esos pagos, rota la API Key desde el portal ahora mismo. |
not_found |
404 | GET de una external_reference que no existe en tu cuenta. | Final. Si crees que sí la enviaste, la petición original nunca llegó: puedes volver a crearla con esa misma referencia. |
wallet_not_provisioned |
404 | GET /v1/wallet en una cuenta que todavía no tiene wallet operativa. | Final hasta que la generemos. Escríbenos. |
deposit_address_label_in_use |
409 | PATCH status=active sobre una dirección de depósito inactiva cuyo label ya lo lleva otra dirección activa (se creó una nueva mientras esta estaba inactiva). | Desactiva la otra primero, o sigue usando la nueva: el label identifica a una sola dirección activa por vez. |
validation |
422 | Campo faltante o mal formado, activo no soportado en esa red, monto cero o dirección inválida. errors dice qué campo. | Final: no se creó nada. Corrige la petición antes de volver a enviarla; repetirla igual devuelve lo mismo. |
address_blocked |
422 | La dirección de destino no está permitida (Términos §5). | Final: no se creó nada y no se va a aceptar. No reintentes; trátalo del lado de tu usuario. |
network_disabled |
422 | POST /v1/payouts en una red que existe pero está deshabilitada (no aparece en GET /v1/networks). | Final mientras la red siga apagada: no se creó nada. Usa una red del catálogo; si la habilitamos de nuevo te avisamos por webhook (network.enabled). |
security_cooldown |
422 | PATCH /v1/account tocando webhook_url, webhook_secret o destination_wallet_address durante el enfriamiento posterior a una recuperación de cuenta (cooldown_until en la respuesta). | Espera a cooldown_until. Si no fuiste tú quien recuperó la cuenta, rota la API Key desde el portal y escríbenos. |
two_factor_required |
422 | PATCH /v1/account cambiando webhook_url, webhook_secret o destination_wallet_address en una cuenta con segundo factor activo, sin two_factor_code. | Repite la petición con two_factor_code (el código de 6 dígitos de tu app). |
two_factor_invalid |
422 | El two_factor_code de ese PATCH no es válido, ya se usó o la cuenta alcanzó el tope diario de códigos fallidos. | Espera al siguiente código de tu app y reintenta. Si no fuiste tú, rota la API Key desde el portal. |
limit_exceeded.max_per_tx |
422 | El monto supera el máximo por transacción de tu cuenta para ese activo: el que acordamos contigo o, si no hay uno más estricto, el piso de seguridad que rige para todas las cuentas (GET /v1/account → effective_limits). | Final para ese monto: no se creó nada. Divide el pago o pídenos subir el límite. |
limit_exceeded.daily_cap |
422 | Sumado a lo ya aceptado hoy (UTC), el monto supera el tope diario de tu cuenta para ese activo. | No es un error del pago y no se creó nada: es cuestión de esperar. Guarda retry_at y vuelve a enviar la misma petición, con la misma external_reference, a partir de ese instante (el cupo se libera al empezar el día UTC siguiente). No lo des por fallido ante tu usuario. |
deposit_address_cap_reached |
429 | POST /v1/deposit-addresses con un label nuevo (o PATCH status=active) cuando tu cuenta ya tiene el máximo de direcciones de depósito activas (cap en la respuesta). | No se creó nada. Desactiva las que ya no uses (PATCH status=inactive) —o espera a que venzan por inactividad— y repite la misma petición con el mismo label. Repetir un label que ya tiene dirección activa nunca da este error. |
Ejemplo: validación (final)
HTTP/1.1 422 Unprocessable Content
{
"message": "The amount field format is invalid.",
"errors": {
"amount": ["The amount field format is invalid."]
},
"code": "validation"
}
Ejemplo: tope diario (reintentar a partir de retry_at)
HTTP/1.1 422 Unprocessable Content
{
"message": "El monto supera el daily_cap para USDT (1000).",
"code": "limit_exceeded.daily_cap",
"retry_at": "2026-09-18T00:00:00.000000Z"
}
9. Precios — GET /v1/pricing
Cobros (payin): 0.2 % del bruto de cada cobro barrido (mínimo 0 USD por barrido) más el gas del barrido al costo real, descontados del propio cobro: tu destino recibe el neto y no queda deuda (§6.4).
Pagos (payout): 0 % del monto más 2 veces el gas real que costó la transacción, convertido a la stablecoin del pago con la cotización del momento. Es deuda de tu cuenta, liquidada a diario hacia nuestro tanque de gas: el destinatario recibe amount entero, nunca se descuenta del pago (fee_amount en GET /v1/payouts y en payout.completed).
Sin mensualidad, sin mínimos de volumen, sin costos ocultos. Los pagos internos (barridos, liquidaciones) no pagan comisión. El precio que rige para un pago es el vigente cuando lo creaste, aunque cambie después.
GET /v1/pricing no requiere API Key; con una API Key válida devuelve el precio de tu cuenta (los acuerdos puntuales aparecen en overridden). Un cambio de precio se programa con fecha de vigencia y se avisa antes: aparece en scheduled_changes y llega por webhook account.pricing_changed (§7.3).
GET https://micalpays.sbs/api/v1/pricing
{
"payin": {
"percentage": "0.2",
"minimum_usd": "0",
"gas": "sweep_gas_at_cost",
"charged_from": "collection"
},
"payout": {
"percentage": "0",
"gas_multiplier": "2",
"charged_as": "daily_settled_debt"
},
"overridden": [],
"scheduled_changes": []
}
Los mismos datos están en el bloque pricing de GET /v1/account (§6.5) y en la pestaña Configuración del portal.
10. Sandbox: integrarte sin blockchain y sin dinero
Cada cuenta tiene una cuenta de prueba gemela con su propia API Key mica_test_… (la ves al registrarte y en Portal → Modo prueba). Misma URL base, mismos endpoints, mismos códigos de error, mismos webhooks y la misma firma HMAC (con el secreto de webhook de la cuenta de prueba): lo que funciona contra el sandbox funciona en producción cambiando solo la llave. La llave decide el ambiente; cada respuesta del sandbox lleva la cabecera X-Micalpays-Environment: sandbox.
Nada toca una blockchain. Las direcciones y los hashes tienen formato válido pero son ficticios, las transacciones se minan solas en unos segundos (un bloque cada 3 s), los payouts y depósitos se confirman con 2 confirmaciones y los explorer_url apuntan a nuestro explorer interno (/sandbox/tx/{hash}). El motor corre cada minuto: un payout tarda alrededor de un minuto en completarse y un depósito simulado, uno o dos.
Para probar: POST /v1/sandbox/fund carga USDT/USDC a tu wallet operativa de prueba (faucet); POST /v1/sandbox/deposits simula que un tercero pagó a una dirección de depósito tuya y dispara deposit.detected → confirmed → swept. Con una API Key real estos endpoints responden 403 sandbox_only. Los datos de prueba se borran a los 30 días; desde el portal puedes vaciarlos cuando quieras.
La cuenta de prueba nace sin URL de webhook, para que los eventos de prueba nunca lleguen a tu servidor de producción: carga la URL de tu entorno de pruebas en Portal → Configuración con el modo prueba activo. Hasta entonces no se envía ningún webhook de prueba.
La cuenta de prueba está activa desde el registro, aunque la real siga pendiente de habilitación. Las comisiones se calculan con la misma fórmula y el mismo precio (§9), con una cotización fija del nativo. No hay Safe ni reposiciones en el sandbox.
POST https://micalpays.sbs/api/v1/sandbox/deposits
Authorization: Bearer mica_test_XXXXXXXXXXXXXXXXXXXXXXXX
Content-Type: application/json
{
"deposit_address_reference": "MPA-…",
"network": "bsc",
"asset": "USDT",
"amount": "25.50"
}
POST https://micalpays.sbs/api/v1/sandbox/fund
{ "network": "bsc", "asset": "USDT", "amount": "1000" }
| Caso | Efecto |
|---|---|
monto de payout terminado en .13 (p.ej. 10.13 USDT) |
La transacción revierte en la cadena simulada: el payout reintenta con backoff y termina failed (webhook payout.failed). |
depósito simulado terminado en .07 (p.ej. 5.07 USDT) |
Llega desde una dirección bloqueada: el depósito queda held (webhook deposit.held) y no se barre. |
destination_address 0x000000000000000000000000000000000000dEaD |
Dirección bloqueada por cumplimiento: POST /v1/payouts responde 422 address_blocked. |
cualquier otro monto |
Camino feliz: queued → processing → completed, con fee_amount calculado como en producción. |
Lista antes de pasar a producción
- Tu receptor de webhooks verifica X-Signature-256 con el secreto de la cuenta REAL y responde 200 a eventos que no conoce.
- Reintentas POST /v1/payouts con la misma external_reference sin duplicar (probaste la idempotencia).
- Manejas failed (payout.failed) y held (deposit.held) del lado de tu usuario.
- Distingues las llaves: mica_test_ en tu entorno de pruebas, mica_ en producción; nunca las dos en el mismo lugar.
- La cuenta real está habilitada (ves "activa" en el portal) y tiene webhook_url y destino de barrido configurados.
¿Algo no coincide con lo que ves en producción? Escríbenos a support@micalpays.sbs — esta página es el contrato y la corregimos si Micalpays hace algo distinto.
Micalpays