Cómo enviar una transferencia cuenta a cuenta (A2A)
Mueve dinero desde tu wallet hacia una cuenta bancaria específica en ACH en línea (antes Transfiya) creando una intent con el esquema a2a.
A diferencia de una dispersión B2P, el destino es la cuenta misma, así que no hay una dirección que resolver antes: digitas el número de cuenta y envías.
Prerrequisitos
Antes de empezar:
- Leíste la guía de Primeros pasos y tienes configurados tu Wallet, Domain y Autenticación.
- Tienes un par de llaves de firma válido para autorizar transacciones.
- Conoces el número de cuenta de destino, su tipo de cuenta y el BICFI del participante receptor, además del tipo y número de documento del titular.
Flujo
Paso 1: Construye la referencia de la cuenta
El destino de la intent es la cuenta receptora, escrita como accountType:accountNumber@transfiya:
svgs:107311223300009@transfiyaEl tipo de cuenta va en minúsculas y debe coincidir con el tipo real de la cuenta en el banco:
| Código | Tipo de cuenta |
|---|---|
svgs | Cuenta de ahorros |
cacc | Cuenta corriente |
dbmo | Depósito de bajo monto |
dord | Depósito ordinario |
dbmi | Depósito de bajo monto inclusivo |
El handle siempre termina en @transfiya, el wallet del riel, igual que las direcciones que usa una dispersión B2P. El participante receptor no va en el handle: viaja en el campo bankBicfi del destino.
En este flujo no hay resolución de direcciones, así que un número de cuenta
equivocado no se detecta antes de enviar. El banco receptor valida la cuenta
cuando recibe la transferencia y la rechaza si la cuenta no existe, lo que el
riel reporta con el código 309. Ante cualquier rechazo los fondos regresan
al origen.
Paso 2: Obtén el BICFI del participante
La lista de participantes está disponible a través del endpoint List Anchors con el filtro kind=a2a. Requiere un token JWT Bearer en el header Authorization.
GET https://<ledger-url>/api/v2/anchors?kind=a2aAuthorization: Bearer <jwt>
x-ledger: <ledger-handle>Respuesta:
{
"data": [
{
"data": {
"handle": "<participant-1>",
"target": "a2a",
"custom": {
"bankBicfi": "<participant-1-bicfi>",
"financialInstitutionName": "<PARTICIPANT-1>"
}
},
"hash": "<hash>",
"meta": { "proofs": ["..."] }
},
{
"data": {
"handle": "<participant-2>",
"target": "a2a",
"custom": {
"bankBicfi": "<participant-2-bicfi>",
"financialInstitutionName": "<PARTICIPANT-2>"
}
},
"hash": "<hash>",
"meta": { "proofs": ["..."] }
},
{
"data": {
"handle": "<participant-3>",
"target": "a2a",
"custom": {
"bankBicfi": "<participant-3-bicfi>",
"financialInstitutionName": "<PARTICIPANT-3>"
}
},
"hash": "<hash>",
"meta": { "proofs": ["..."] }
}
],
"page": { "total": 3, "index": 0, "limit": 3 }
}Cada entrada de data es un participante, y el arreglo trae tantos como estén habilitados, así que su longitud cambia a medida que se suman participantes al riel. Los códigos están en data[i].data.custom.bankBicfi y los nombres en data[i].data.custom.financialInstitutionName; data[i].data.handle también trae el nombre. Envía el bankBicfi del participante de destino como el bankBicfi del destino de la transferencia.
En Participantes activos puedes ver cuáles participantes están habilitados hoy.
Paso 3: Crea la intent de transferencia
El esquema a2a lleva la identidad de ambos extremos de la transferencia: quién envía y quién recibe. Esos campos viajan en source.custom y target.custom.
Definición de campos
| Nombre | Tipo | Descripción |
|---|---|---|
| handle* | String (36) | Identificador único de la intent, y su llave de idempotencia. Los handles son únicos en todo el sistema, sin ventana de expiración |
| schema* | String | Solo admite: a2a |
| claims[].action* | String | Solo admite: transfer |
| claims[].amount* | Integer | Monto de la transferencia en unidades menores. Mínimo 100 |
| claims[].symbol.handle* | String | Solo admite: tcop |
| claims[].source.handle* | String | Tu wallet, de donde se debitan los fondos |
| claims[].source.custom.name* | String (160) | Nombre completo del originador |
| claims[].source.custom.idType | String | Tipo de documento del originador. Acepta: CC CE PA TI NUIP NIT OTR PPT PEP |
| claims[].source.custom.idValue | String (34) | Número de documento del originador |
| claims[].source.custom.bankBicfi | String (34) | BICFI del participante de origen. No se envía en una transferencia que tú originas; aparece en el origen de una transferencia entrante |
| claims[].target.handle* | String (80) | Cuenta de destino, por ejemplo: svgs:107311223300009@transfiya |
| claims[].target.custom.idType* | String | Tipo de documento del beneficiario. Acepta: CC CE PA TI NUIP NIT OTR PPT PEP |
| claims[].target.custom.idValue* | String (34) | Número de documento del beneficiario |
| claims[].target.custom.name | String (160) | Nombre completo del beneficiario |
| claims[].target.custom.bankBicfi | String (34) | BICFI del participante de destino, tomado de la lista de participantes del paso 2 |
| custom.description | String (80) | Descripción de la transferencia |
| custom.externalTxId | String | Tu propio identificador de la transacción (CUS) |
amount va en unidades menores: multiplica el valor en pesos por el factor
(100) del símbolo tcop. COP 1.000 se envía como 100000, y el 12500000
del ejemplo de abajo equivale a COP 125.000. El valor mínimo aceptado es
100, que equivale a COP 1.
El límite es de COP 250.000.000 por transferencia, y estas transferencias se liquidan en tiempo real. El riel funciona 24/7: no hay horarios ni cortes.
Ejemplo de solicitud
Genera el hash, firma y envía al endpoint Create Intent. Consulta Hashing y firma de solicitudes para más detalle.
POST https://<ledger-url>/api/v2/intentsAccept: application/json, text/plain, */*
Content-Type: application/json
x-ledger: <ledger-handle>{
"data": {
"handle": "a2a-order-000123",
"schema": "a2a",
"claims": [
{
"action": "transfer",
"source": {
"handle": "<source-wallet-handle>",
"custom": {
"name": "Negocio Ejemplo SAS",
"idType": "NIT",
"idValue": "900123456"
}
},
"target": {
"handle": "svgs:107311223300009@transfiya",
"custom": {
"name": "Juan Ejemplo",
"idType": "CC",
"idValue": "1020304050",
"bankBicfi": "<destination-participant-bicfi>"
}
},
"symbol": { "handle": "tcop" },
"amount": 12500000
}
],
"access": [
{ "action": "any", "signer": { "public": "<public-key>" } }
],
"custom": {
"description": "Pago a proveedor 000123",
"externalTxId": "TX-000123"
}
},
"hash": "<sha256-hash-of-data>",
"meta": {
"proofs": [
{
"method": "ed25519-v2",
"public": "<public-key>",
"digest": "<digest-of-proof>",
"result": "<signature-of-digest>",
"custom": { "moment": "<iso-8601-timestamp>" }
}
]
}
}Paso 4: Escucha la finalización
Registra un Effect en la señal intent-updated. Filtrar por el esquema mantiene el handler acotado a las transferencias cuenta a cuenta, y emparejar ambos estados finales hace que también te enteres de un rechazo, donde los fondos regresan al origen.
Genera el hash, firma y envía al endpoint Create Effect.
POST https://<ledger-url>/api/v2/effectsAccept: application/json, text/plain, */*
Content-Type: application/json
x-ledger: <ledger-handle>{
"data": {
"handle": "a2a-intent-completion@<domain>",
"signal": "intent-updated",
"filter": {
"intent.data.schema": "a2a",
"intent.meta.status": {
"$in": ["completed", "rejected"]
}
},
"action": {
"schema": "webhook",
"endpoint": "<webhook-endpoint>"
},
"access": [
{
"action": "any",
"signer": { "public": "<public-key>" }
}
]
},
"hash": "<sha256-hash-of-data>",
"meta": {
"proofs": [
{
"custom": { "moment": "<iso-8601-timestamp>" },
"method": "ed25519-raw",
"public": "<public-key>",
"result": "<hash-signature>"
}
]
}
}El handle del effect debe incluir tu dominio, como en
a2a-intent-completion@<domain>. Sin el sufijo @<domain> el effect no se
puede registrar contra tu wallet.
Para los payloads de entrega y cómo leer un rechazo, consulta Cómo manejar los webhooks de ACH en línea.
Cuando una transferencia se rechaza, el proof de failed trae un reason y el código propio del riel en detail. Ambos están listados en Errores del bridge, la sección de la referencia de errores que aplica a este riel.
Una intent a2a pasa por los estados estándar, created → pending → prepared → committed → completed, sin ningún estado propio del riel. Consulta Ciclo de vida de una intent.
Revisa las Consideraciones y restricciones para conocer los formatos de número de cuenta de bancos específicos antes de enviar en producción.
Historial de cambios
- Agregado• Versión inicial