SEP-24SEP-6USDCStellarColombia · COP

Anchor Stellar de VANK — Guía de integración

Conecta tu wallet o exchange al peso colombiano. Tus usuarios convierten USDC ⇄ COP con pagos PSE de entrada y Bre-B de salida — VANK maneja el cumplimiento, el KYC y la liquidación.

1 · Qué es el Anchor de VANK

Un anchor es la pieza que conecta la red Stellar con el dinero local. El Anchor de VANK conecta USDC (el dólar digital emitido por Circle) con el peso colombiano, usando los protocolos estándar del ecosistema Stellar: SEP-10 para autenticación y SEP-24 para depósitos y retiros interactivos.

⬇️ Depósito (on-ramp) · COP → USDC

Tu usuario paga pesos por PSE desde su banco y recibe USDC en su cuenta Stellar. VANK cobra el fiat, ejecuta el cumplimiento y entrega el USDC on-chain.

⬆️ Retiro (off-ramp) · USDC → COP

Tu usuario envía USDC al anchor y recibe pesos al instante en su llave Bre-B — el sistema de pagos inmediatos de Colombia. La cotización se muestra antes de confirmar, sin spread oculto.

Tu integración habla los estándares del ecosistema: si tu wallet ya se conecta a otros anchors (MoneyGram Ramps, por ejemplo), conectarse a VANK es el mismo código apuntando a otro dominio. Y eliges la vía: SEP-24, donde abres nuestro formulario y nosotros guiamos al usuario, o SEP-6, donde todo va por API y la pantalla la pones tú.

2 · Integración en 6 pasos

  1. 1

    Empieza en testnet — no pides permiso

    El ambiente de pruebas está abierto: apunta tu integración a dev-stable-anchor.thisisvank.com y arranca hoy. Escribirnos hace falta después, para pasar a producción.

  2. 2

    Prepara tu wallet de testnet

    Una cuenta Stellar en testnet con trustline a USDC de prueba. El emisor está en el stellar.toml del ambiente de pruebas.

  3. 3

    Implementa SEP-10 y elige tu vía

    Primero la autenticación (SEP-10). Luego eliges una de dos: con SEP-24 abres nuestro formulario y nosotros guiamos al usuario, que son pocas líneas con el Stellar Wallet SDK. Con SEP-6 todo va por API y la pantalla la pones tú. El código de las dos está abajo, y SEP-6 tiene su propia sección.

  4. 4

    Certifica en testnet

    Tres casos que haces tú mismo: un depósito, un retiro y un retiro con devolución. En la sección 7 está qué prueba cada uno y qué miramos nosotros.

  5. 5

    Aprobación

    VANK revisa tu certificación y tu caso de uso, y coordina contigo el acuerdo de integración.

  6. 6

    Producción

    Entregas el dominio definitivo de tu app y pasas a mainnet: anchor.thisisvank.com, con el USDC de Circle.

3 · Iniciar una transacción

SEP-1 · stellar.toml

Todo empieza en el stellar.toml del ambiente: ahí están TODOS los endpoints que vas a usar, la llave con la que el anchor firma y el activo soportado. No hardcodees ninguno de estos valores — léelos del toml, que es la fuente de verdad y cambia entre ambientes.

Campo del tomlProtocoloPara qué lo necesitas
WEB_AUTH_ENDPOINTSEP-10Autenticarte. Es el primer paso de todo lo demás.
TRANSFER_SERVERSEP-6Depósito y retiro por API, sin nuestro formulario. Es el endpoint de la sección 5. Este campo es además el que te dice si SEP-6 está habilitado en ese ambiente: si el toml lo publica, está.
TRANSFER_SERVER_SEP0024SEP-24Depósito y retiro abriendo nuestro formulario.
KYC_SERVERSEP-12Identidad del usuario. Y en el retiro por SEP-6, por aquí viaja la llave Bre-B de destino.
ANCHOR_QUOTE_SERVERSEP-38Cotizaciones en firme, para garantizarle la tasa a tu usuario.
SIGNING_KEYSEP-10La llave pública con la que el anchor firma el challenge. Verifícala.
CURRENCIESSEP-1El USDC soportado y su EMISOR, que cambia entre testnet y producción.
Pruebas (testnet)Producción (mainnet)
Dominio del anchordev-stable-anchor.thisisvank.comanchor.thisisvank.com
stellar.toml/.well-known/stellar.toml/.well-known/stellar.toml
RedStellar testnetStellar mainnet
Emisor de USDCde prueba — léelo del tomlCircle
⚠️ El emisor del USDC cambia entre ambientes. Léelo siempre del stellar.toml — nunca lo escribas fijo en tu código.
curl
# Lee el stellar.toml del ambiente — es la fuente de verdad
curl https://dev-stable-anchor.thisisvank.com/.well-known/stellar.toml   # producción: anchor.thisisvank.com

SEP-10 · Autenticación

SEP-10 prueba que controlas la cuenta Stellar: pides un challenge, lo firmas y lo canjeas por un JWT que autoriza el resto de llamadas. Ojo con esto, que es lo que más se pasa por alto: el challenge se firma con DOS llaves. La de la cuenta del usuario, que dice quién opera, y la de TU DOMINIO, que dice qué app lo origina. La segunda es requisito de acceso y se explica abajo.

curl
# Sin el SDK, SEP-10 son dos llamadas (cualquier lenguaje):
GET  https://dev-stable-anchor.thisisvank.com/auth?account=G...        # → { transaction, network_passphrase }
#    Verifica el challenge ANTES de firmar: read_challenge_transaction(xdr, SIGNING_KEY del toml, passphrase, home_domain, web_auth_domain) en el SDK de tu lenguaje
#    Fírmalo con la llave de la cuenta (y con la de tu dominio si mandaste client_domain)
POST https://dev-stable-anchor.thisisvank.com/auth   {"transaction": "<xdr firmado>"}   # → { token }  (JWT, 24 h)
shell
yarn add @stellar/typescript-wallet-sdk
Identidad de tu app (client_domain) — en el sandbox es OPCIONAL (puedes autenticarte solo con la llave de la cuenta y seguir); para producción es obligatorio: tu dominio debe publicar su propio stellar.toml en https://tu-dominio/.well-known/stellar.toml con una SIGNING_KEY, y tu autenticación SEP-10 debe enviar ese clientDomain (tu backend co-firma el challenge con esa llave; el Wallet SDK lo soporta nativamente). Es el mecanismo estándar del ecosistema — el mismo que usas con otros anchors — y es como el anchor sabe, con prueba criptográfica y no por declaración, qué wallet origina cada transacción.
TypeScript
import { Wallet, SigningKeypair, DomainSigner } from "@stellar/typescript-wallet-sdk";

const HOME_DOMAIN = "dev-stable-anchor.thisisvank.com"; // pruebas · producción: anchor.thisisvank.com
const CLIENT_DOMAIN = "tu-dominio.com";                 // el tuyo, el que publica tu SIGNING_KEY

async function authenticate(authSecretKey: string) {
  const wallet = Wallet.TestNet(); // producción: Wallet.MainNet()
  const anchor = wallet.anchor({ homeDomain: HOME_DOMAIN });
  const sep10 = await anchor.sep10();
  const authKey = SigningKeypair.fromSecret(authSecretKey);

  // Tu backend co-firma el challenge con la llave de tu dominio.
  // La llave SECRETA nunca sale de ahí, ni llega al navegador.
  const walletSigner = new DomainSigner("https://" + CLIENT_DOMAIN + "/sign", {});

  return await sep10.authenticate({
    accountKp: authKey,
    walletSigner,
    clientDomain: CLIENT_DOMAIN,
  });
}

El /sign es tuyo y vive en tu backend. Recibe el challenge, lo firma con la llave secreta de tu dominio y lo devuelve. Esa llave secreta nunca sale de tu servidor ni llega al navegador — por eso la firma la hace el backend y no el cliente:

TypeScript
// En TU backend. Recibe el challenge, lo firma con la llave de tu dominio y lo devuelve.
// POST /sign  →  { transaction, network_passphrase }
import { Transaction, Keypair } from "@stellar/stellar-sdk";

app.post("/sign", (req, res) => {
  const { transaction, network_passphrase } = req.body;
  const tx = new Transaction(transaction, network_passphrase);
  tx.sign(Keypair.fromSecret(process.env.CLIENT_DOMAIN_SECRET!)); // la secreta de tu SIGNING_KEY
  res.json({ transaction: tx.toXDR(), network_passphrase });
});

El archivo es mínimo — esto es TODO lo que necesita contener:

toml
# https://tu-dominio/.well-known/stellar.toml
VERSION = "2.7.0"
SIGNING_KEY = "G...LA_LLAVE_PUBLICA_DE_TU_APP"
  • SIGNING_KEY es la llave PÚBLICA de un par Stellar que tu equipo controla. La secreta nunca se publica: es la que tu backend usa para co-firmar el challenge SEP-10.
  • Genera el par con el SDK o el Stellar Lab. La cuenta no necesita fondos ni existir on-chain — solo identifica a tu app.
  • Debe responder por https público, exactamente en esa ruta, sin autenticación y con CORS abierto (Access-Control-Allow-Origin: *). El anchor lo consulta en cada autenticación.
  • ¿Tu app ya publica un stellar.toml por otras razones? Perfecto: solo asegúrate de que incluya SIGNING_KEY — no necesitas ningún otro campo para conectarte a VANK.

SEP-24 · Depósito y retiro interactivos

La llamada devuelve una URL y un id. Abres la URL en un webview y ahí el usuario ve el desglose completo antes de confirmar: la tasa final, la comisión y el IVA POR SEPARADO, el total neto que va a recibir, y un contador con el tiempo real que le queda a esa tasa. Nada va escondido dentro del precio. El id es tu manija para consultar el estado.

TypeScript
// Retiro (off-ramp): el usuario envía USDC y recibe COP
const { url, id } = await anchor.sep24().withdraw({
  authToken,
  withdrawalAccount: USER_STELLAR_PUBLIC_KEY,
  assetCode: "USDC",
  lang: "es",
});
// Abre `url` en un webview: la UI de VANK guía el resto
TypeScript
// Depósito (on-ramp): el usuario paga COP (PSE) y recibe USDC
const { url, id } = await anchor.sep24().deposit({
  authToken,
  destinationAccount: USER_STELLAR_PUBLIC_KEY,
  assetCode: "USDC",
  lang: "es",
});
El enlace hay que abrirlo dentro de los 10 minutos siguientes a pedirlo: ese es el tiempo que vive su token. Una vez abierto, el formulario sigue funcionando aunque el token venza — el límite es abrirlo, no completarlo. Y una transacción que se queda en «incomplete» es un formulario que se abrió y no se envió: no la trates como una operación en curso.

4 · Estados y qué hacer en cada uno

Consulta el estado con el watcher del SDK (recomendado) o con el endpoint crudo. No hay webhooks hacia tu URL: el parámetro on_change_callback se acepta pero no dispara nada, así que sondea. Un 403 dice solo forbidden: el JWT falta, es inválido o venció (dura 24 h); vuelve a hacer SEP-10. El ciclo es el estándar SEP-24:

TypeScript
const watcher = anchor.sep24().watcher();
const { stop } = watcher.watchOneTransaction({
  authToken,
  assetCode: "USDC",
  id: transactionId,
  onMessage: (tx) => {
    if (tx.status === "pending_user_transfer_start") {
      // Retiro: envía el USDC ahora. Depósito: espera el pago del usuario
    }
  },
  onSuccess: (tx) => { /* completed */ },
  onError: (tx) => { /* error */ },
});
curl
# Sin el SDK: iniciar y consultar (sandbox; producción: anchor.thisisvank.com)
curl -X POST "https://dev-stable-anchor.thisisvank.com/sep24/transactions/deposit/interactive" \
  -H "Authorization: Bearer $SEP10_JWT" -H "Content-Type: application/json" \
  -d '{"asset_code":"USDC","account":"G...","lang":"es"}'      # o /withdraw/interactive → { url, id }

curl "https://dev-stable-anchor.thisisvank.com/sep24/transaction?id=$TRANSACTION_ID" \
  -H "Authorization: Bearer $SEP10_JWT"
EstadoQué significaTu acción
incompleteEl formulario se abrió y aún no se envía.Nada. NO expira sola: se queda ahí indefinidamente. Si tu usuario la abandonó, inicia otra — y para retomarla tienes que pedir el enlace de nuevo.
pending_user_transfer_startRetiro: el anchor espera tu USDC. Depósito: espera el pago PSE del usuario.Retiro: envía el USDC a la cuenta y memo que entrega la transacción. Depósito: nada.
pending_anchorVANK recibió los fondos y está procesando.Nada — sigue el estado.
pending_externalRetiro: los pesos van en camino por Bre-B.Nada — normalmente segundos. Si el proveedor falla, la transacción pasa a error y la devolución sale sola.
completedEntregado: COP en la cuenta del usuario o USDC en su wallet.Muestra el comprobante (more_info_url).
errorLa operación no pudo completarse.Muestra el motivo; si había USDC recibido, la devolución es automática.

Devoluciones

Si un retiro no puede completarse después de recibir el USDC, la devolución a la cuenta de origen es automática, sin intervención humana y por el importe íntegro: el servicio no se prestó, así que no se descuenta comisión.

Cumplimiento

Cada operación pasa por verificación de identidad y screening del beneficiario (listas restringidas, PEP, OFAC) antes de mover dinero. El control es fail-closed: si la verificación no puede ejecutarse, la operación no avanza.

5 · SEP-6: la vía por API

Todo lo anterior usa SEP-24: tu app abre nuestro formulario y nosotros guiamos al usuario. Con SEP-6 tu app pide, pregunta y confirma por API, y la pantalla la pones tú. El RETIRO es enteramente programático: no se abre nada en ningún momento. El DEPÓSITO termina en un enlace, y no por capricho nuestro: el pago en pesos se hace por PSE, donde la persona elige su banco, así que no existe una URL de pago antes de esa elección. Abajo está exactamente de dónde sale ese enlace.

⚠️ SEP-6 se habilita por ambiente, y el propio stellar.toml te lo dice sin que tengas que preguntarnos: si publica TRANSFER_SERVER, SEP-6 está disponible ahí; si no lo publica, todavía no. El ambiente de pruebas ya lo sirve. Compruébalo en el toml antes de apuntar tu integración a un ambiente nuevo — es la misma regla que ya aplicas para el emisor del USDC. SEP-24 está disponible en todos.
Lo que más confunde al integrar en SEP-6 la petición de retiro NO lleva el destino. A dónde va el dinero viaja aparte, por SEP-12, en el campo estándar bank_account_number, y te lo pedimos en CADA retiro: nunca reutilizamos el anterior. Mándalo con transaction_id; si lo envías sin él, lo toma el retiro de esa cuenta que esté esperando llave. Y ojo con quién es quién en ese SEP-12: los campos de identidad (nombre, documento, correo) son de TU USUARIO, el que retira, igual que en SEP-24 los escribe en nuestro formulario. Del BENEFICIARIO solo mandas la llave: su nombre y documento los resolvemos nosotros contra el directorio Bre-B y los screeneamos aparte. Te los devolvemos en message para que se los enseñes a tu usuario antes de firmar. Son dos chequeos sobre dos personas a propósito, quien envía y quien recibe: ninguno de los dos puede tener problemas legales.

Qué instalar

shell
npm install @stellar/stellar-sdk        # firmar SEP-10 y el pago on-chain
# Nada más: el resto de SEP-6 son llamadas HTTP normales (fetch).

Un retiro, paso a paso

Autentícate primero con SEP-10, igual que en el paso 3 de esta guía. El resto son llamadas HTTP:

TypeScript
const ANCHOR = "https://dev-stable-anchor.thisisvank.com";   // SEP-6 vive HOY en el ambiente de pruebas
const auth = { Authorization: `Bearer ${authToken}` };   // JWT de SEP-10 (paso 3)

// 1) Pide el retiro. Devuelve un id; todavía NO puedes enviar el USDC.
const { id } = await fetch(
  `${ANCHOR}/sep6/withdraw?asset_code=USDC&account=${account}&amount=2&type=breb`,
  { headers: auth },
).then((r) => r.json());

// 2) Sondea la transacción y REACCIONA a su estado.
const tx = async () =>
  (await fetch(`${ANCHOR}/sep6/transaction?id=${id}`, { headers: auth }).then((r) => r.json()))
    .transaction;

// Recién creada está en incomplete: espera el PRIMER ciclo del motor (hasta ~30 s) antes de decidir nada. Si preguntas una sola vez ahora, te saltas el paso siguiente.
let t = await tx();
while (t.status === "incomplete") { await sleep(5000); t = await tx(); }
if (t.status === "pending_customer_info_update") {
  // 3) Te faltan datos. PREGUNTA cuáles — nunca los adivines.
  const need = await fetch(
    `${ANCHOR}/sep12/customer?account=${account}&transaction_id=${id}`,
    { headers: auth },
  ).then((r) => r.json());
  console.log(need.fields);   // cada campo trae una descripción para tu usuario

  // 4) Envíalos ATADOS a esta transacción. bank_account_number = la llave Bre-B.
  await fetch(`${ANCHOR}/sep12/customer`, {
    method: "PUT",
    headers: { ...auth, "Content-Type": "application/json" },
    body: JSON.stringify({
      account,
      transaction_id: id,
      type: "sep6-withdrawal",   // contexto de retiro: así SEP-12 sabe que hace falta la llave
      // TU USUARIO, quien retira. Lo mismo que escribe en nuestro formulario en SEP-24.
      first_name: "Ana", last_name: "Pérez",
      email_address: "ana@example.com",
      id_type: "CC", id_number: "1000000001",
      // EL BENEFICIARIO: solo su llave. Su nombre y documento NO los mandas — los resolvemos nosotros contra el directorio Bre-B.
      bank_account_number: "@llaveDeTuUsuario",
    }),
  });
}

// 5) SONDEA — no esperes una respuesta inmediata. Tras el PUT, la transacción
//    puede tardar hasta ~30 s en avanzar: nuestro motor revisa por ciclos.
while (["incomplete", "pending_customer_info_update"].includes((t = await tx()).status)) await sleep(5000);
// Cuando pase a pending_user_transfer_start ya tienes destino y memo.
// t.withdraw_anchor_account · t.withdraw_memo (memo_type "id") · t.amount_out = COP netos
// t.fee_details.total viene en USDC: es el mismo fee que la cotización te dio en COP, en otra unidad
// t.message = el beneficiario resuelto: enséñaselo al usuario ANTES de firmar

// 6) Envía el USDC a esa cuenta con ESE memo, y sigue sondeando hasta completed.
Sondea, no supongas. Ninguna de estas llamadas cambia el estado al instante: nuestro motor revisa las transacciones por ciclos, así que después de mandar los datos del cliente la transacción puede tardar hasta unos 30 segundos en avanzar. Si tu app concluye que falló porque el estado no cambió al momento, vas a mostrarle un error a alguien cuyo retiro va perfectamente.

Los estados que vas a ver

EstadoQué significa y qué haces
incompleteRETIRO: recién creada; estamos calculando y verificando, avanza en el siguiente ciclo, hasta ~30 s. DEPÓSITO: aquí se QUEDA, y es lo correcto — esperamos a que tu usuario pague por el enlace. No la esperes avanzar sola.
pending_customer_info_updateNos faltan datos del cliente. Consulta GET /sep12/customer con este transaction_id, muestra los campos a tu usuario y mándalos con PUT. El campo message de la transacción te dice qué falta.
pending_user_transfer_startTodo listo. Envía el USDC a withdraw_anchor_account con withdraw_memo. Antes de firmar, enseña el beneficiario que viene en message.
pending_anchor · pending_externalRecibimos tu USDC y el pago en pesos está en curso. Solo esperar.
completedEl beneficiario recibió los pesos.
errorNo se pudo continuar y message dice por qué. Si ya habías enviado el USDC, la devolución es automática e íntegra.

El depósito

Hay dos formas de iniciar un depósito, y no atan lo mismo:

Endpointamount va enQué queda atadoCuándo usarlo
GET /sep6/depositUSDCNada. Es una intención: el formulario abre con el monto editable, tu usuario escribe los pesos que va a pagar y se liquida a la tasa del momento.Cuando no necesitas prometerle un número a tu usuario. Ojo: amount=5000 aquí son cinco mil dólares.
GET /sep6/deposit-exchange + quote_idCOPMonto y tasa. El formulario abre con los dos bloqueados, y lo que se prometió es lo que llega.Cuando le garantizas la tasa a tu usuario. Es la del código de abajo.

Las dos devuelven lo mismo, y es lo que más confunde: solo un id y un texto fijo que dice que mires la transacción. El enlace no viene ahí. Está en la transacción, en el campo more_info_url, ya firmado. Lo abres en un webview, tu usuario elige su banco y paga por PSE, y cuando el pago se confirma el USDC llega a su cuenta Stellar y la transacción pasa a completed. Mínimo recomendado: 2.000 COP, porque la comisión del depósito son 1.785 y se descuenta antes de convertir.

TypeScript
// 1) Cotización en firme (opcional, pero es lo que le garantiza la tasa a tu usuario)
// USDC_ISSUER: el emisor que leíste del stellar.toml del ambiente (sección 3). Nunca fijo en el código.
const q = await fetch(`${ANCHOR}/sep38/quote`, {
  method: "POST",
  headers: { ...auth, "Content-Type": "application/json" },
  body: JSON.stringify({
    sell_asset: "iso4217:COP", buy_asset: `stellar:USDC:${USDC_ISSUER}`,
    sell_amount: "5000", sell_delivery_method: "bank_transfer",
    country_code: "CO", context: "sep6",
  }),
}).then((r) => r.json());
// q.price · q.buy_amount · q.fee.total  ← la comisión viene APARTE, no dentro del precio

// 2) Inicia el depósito con esa cotización.
const { id } = await fetch(
  `${ANCHOR}/sep6/deposit-exchange?amount=5000&destination_asset=USDC` +
  `&source_asset=iso4217:COP&quote_id=${q.id}&account=${account}&type=bank_transfer`,
  { headers: auth },
).then((r) => r.json());
// OJO: esta respuesta NO trae el enlace. Solo { how, id }.

// 3) El enlace está en la TRANSACCIÓN.
const t = await fetch(`${ANCHOR}/sep6/transaction?id=${id}`, { headers: auth })
  .then((r) => r.json()).then((r) => r.transaction);
// t.more_info_url            → ábrelo en un webview. Su token vive 10 min; si vence, relee y sale otro.
// t.user_action_required_by  → hasta cuándo tiene tu usuario. HOY viene null: usa expires_at de la cotización

// 4) Tu usuario elige banco y paga por PSE. Sigue sondeando hasta completed.

Después del enlace: qué pasa y qué ves

Tu app abrió el more_info_url en un webview y sigue sondeando GET /sep6/transaction. Esto es lo que hace tu usuario en cada momento y el estado que tu sondeo va a ver mientras tanto.

MomentoEstado que ve tu sondeoQué está pasando
Abre el enlaceincompleteVe el monto bloqueado, la tasa congelada, la comisión y el IVA por separado y el neto en USDC, con el contador real. Escribe sus datos, autoriza el KYC (corre sobre la cédula que escribe), elige su banco y pulsa Continuar.
Envió el formulariopending_user_transfer_startCreamos la orden y la cotización quedó consumida: aquí termina el plazo de los 10 minutos. Lo enviamos al portal de su banco (PSE). Ese pago puede tardar lo que tarde.
El banco confirmópending_anchorRecibimos los pesos y estamos enviando el USDC a su cuenta Stellar.
Listocompletedstellar_transaction_id trae el hash y message dice «USDC enviado on-chain». Tu usuario ve «Pago confirmado» con ese mismo hash. Muéstraselo.
Lo abandonóincompleteSe queda ahí, no expira sola. Si quiere pagar más tarde, inicia otro depósito: el enlace y la cotización ya vencieron.

Precio cerrado antes de mover el dinero

Cada operación de SEP-6 tiene dos versiones: la normal y la -exchange. La diferencia es una sola cosa, si la tasa está cerrada antes de mover el dinero o no:

Sin cotizaciónCon cotización en firme
Endpoints/sep6/deposit · /sep6/withdrawPrimero POST /sep38/quote, que te da un quote_id. Luego /sep6/deposit-exchange o /sep6/withdraw-exchange con ese quote_id.
La tasaLa del momento en que se liquida. Puede moverse entre que pides la operación y que se completa.La de la cotización, congelada para esa transacción. Un solo uso, no sirve para otra.
Lo que le puedes prometer a tu usuarioNada exacto. El número lo ve al final.El número exacto antes de que confirme: la cotización trae el precio y la comisión por separado, y lo prometido es lo que llega.
Cuándo usarlaCuando tu app no muestra un monto antes de operar.Cuando tu app le dice «vas a recibir X» antes de que pague o envíe. Es lo que hace cualquier app seria.
Cuánto duraNo aplica.10 minutos en el depósito, porque la persona abre el enlace, se verifica y elige banco. 5 minutos en el retiro, que es programático.

Qué tiene que pasar dentro del plazo: en el depósito, que tu usuario envíe el formulario; el pago en PSE puede tardar más. En el retiro, que mandes la llave y los datos por SEP-12; nuestro motor consume la cotización al crear la orden.

Cómo probarlo

Con los datos de prueba de la sección 7, contra el ambiente de pruebas. Dos cosas que conviene comprobar en el sandbox porque son las que sorprenden en producción: haz DOS retiros seguidos y verifica que en el segundo te volvemos a pedir la llave, y haz uno de 1.11 USDC para ver la devolución automática. Si pruebas el DEPÓSITO con la Demo Wallet del SDF, ten presente que esa herramienta recibe el more_info_url y no te lo muestra: solo registra el estado de la transacción. Saca de sus logs el token de POST /auth y el id del depósito, pide GET /sep6/transaction?id=… con ese token, y ahí está el enlace. Al pagar, la Demo Wallet detecta el completed y cierra sola.

bash
# SEP-6 · depósito — NO necesita SEP-12: la identidad se verifica en el formulario del more_info_url
GET /sep6/deposit?asset_code=USDC&account=G...&amount=2&type=bank_transfer
#   → paga por el more_info_url de la transacción (mismo formulario y mismo PSE de prueba)

# SEP-6 · retiro — SÍ necesita SEP-12: PUT /sep12/customer (Authorization: Bearer <JWT SEP-10>)
{
  "account": "G...TU_CUENTA_TESTNET",
  "type": "sep6-withdrawal",
  "first_name": "Tester",                     # quien retira: tu usuario
  "last_name": "Sandbox",
  "email_address": "tester@example.com",
  "id_type": "CC",
  "id_number": "1000000001",
  "bank_account_number": "@alphamunKey01"     # el beneficiario: solo su llave Bre-B
}
GET /sep6/withdraw?asset_code=USDC&account=G...&amount=1&type=breb   # amount=1.11 → con devolución

6 · ¿No manejas llaves Stellar? Cuentas custodiales

Si tu producto no es una wallet Stellar, VANK también puede crear y custodiar una cuenta Stellar por cada usuario tuyo: tú la referencias con tu propio identificador y nunca tocas llaves privadas — VANK las gestiona de forma segura. Es la vía rápida para fintechs que quieren ofrecer USDC sin operar infraestructura cripto.

El acceso a la integración custodial sigue el mismo proceso de solicitud y certificación de esta guía — escríbenos y te compartimos la documentación técnica específica.

7 · Certificación y paso a producción

La certificación no es un trámite que esperas: la haces tú, hoy, en el sandbox abierto. Son tres casos, y cada uno demuestra que tu integración maneja bien un momento distinto. Aquí está qué probar y, sobre todo, qué vamos a mirar — para que sepas si vas a pasar antes de enviárnoslo.

CasoQué demuestraQué miramos
1 · Un depósitoQue llevas al usuario a pagar y acreditas el USDC en su cuenta.Que el USDC llegó a la cuenta que declaraste, y que tu app mostró el monto en pesos antes de que la persona pagara.
2 · Un retiroQue pides el destino, envías el USDC al lugar correcto y sigues la operación hasta el final.Que le enseñaste al usuario el beneficiario resuelto ANTES de firmar, y que el neto que mostraste coincide con el que pagamos.
3 · Un retiro con devoluciónQue manejas el caso en que el pago no sale. Retira exactamente 1.11 USDC: el sandbox lo rechaza a propósito.Que reaccionaste al estado error, que le explicaste al usuario con nuestro mensaje, y que reflejaste el USDC devuelto en vez de darlo por perdido.
Cuando los tengas, envía los tres transaction IDs a contacto@vank.co. Revisamos cada uno contra lo de arriba y te respondemos con lo que encontramos: si algo falla te decimos exactamente qué, no un rechazo genérico.

Después: pasar a producción

  1. Crea tu cuenta en VANK (app.vank.co) y escríbenos a contacto@vank.co con el dominio de tu app, qué construyes y tu caso de uso.
  2. Requisito para conectarte: tu dominio debe publicar el archivo público https://tu-dominio/.well-known/stellar.toml con la SIGNING_KEY de tu app, y tu autenticación SEP-10 debe enviar ese clientDomain (co-firmando el challenge — el Wallet SDK lo soporta). Es el estándar del ecosistema y es lo que nos permite atribuirte tus transacciones.
  3. Registramos tu dominio, firmamos el acuerdo de integración y entras a mainnet con el USDC de Circle.

Datos de prueba del sandbox

Todo lo que necesitas para completar la certificación sin esperarnos. Son datos ficticios y compartidos: no hay ninguna persona ni dinero real detrás, y no debes usar documentos reales en el sandbox.

QuéValorNota
Ambientedev-stable-anchor.thisisvank.comStellar testnet. Usa tu propia wallet en testnet (o la Demo Wallet del SDF).
Fondos de testnetXLM: Friendbot · USDC: trustline al emisor del stellar.toml y pídelo en https://faucet.circle.com (red Stellar testnet)Primero la trustline, luego el faucet de Circle: sin trustline el USDC no entra. Tu propio depósito de prueba también te deja USDC.
Identidad para el KYCTester Sandbox · CC 1000000001Identidad ficticia con verificación instantánea. Correo y teléfono: cualquiera.
Depósito (PSE de prueba)≥ 5.000 COP · banco BANCO UNION COLOMBIANO · el min_amount de /info va en USDCCon menos, la comisión fija del sandbox se come el monto. Sigue los pasos del PSE de prueba de abajo.
Retiro (Bre-B de prueba)llave @alphamunKey01 · ≥ 1 USDCTitular de prueba del directorio Bre-B del sandbox. Te la pediremos en CADA retiro: el destino nunca se hereda del anterior.
Retiro con devoluciónmisma llave · exactamente 1.11 USDCEse monto lo rechaza el sandbox a propósito: verás la transacción en error y el USDC de vuelta en tu cuenta, solo.
Por API (SEP-6 / SEP-12)first_name=Tester last_name=Sandbox id_type=CC id_number=1000000001Para el retiro, la llave Bre-B va en el campo bank_account_number.

Formulario de depósito, campo por campo (todos los campos con * son obligatorios; escribe exactamente esto):

CampoQué escribirNota
Monto a depositar *5000En COP. Menos de eso y la comisión fija del sandbox se lo come.
Nombre completo *Tester Sandbox
Tipo de documento *CC · Cédula de CiudadaníaEs la primera opción de la lista.
Número de documento *1000000001Solo dígitos, sin puntos.
Correo electrónico *tester@example.comCualquiera con formato de correo.
Teléfono *3001234567Cualquier número.
Autorizo la verificación de identidad (KYC) *marca la casillaEn segundos aparece «Identidad verificada · KYC completado».
Método de pago *PSE · banco BANCO UNION COLOMBIANOExactamente ese: la lista también trae «BANCO UNION» y «BANCO UNION COLOMBIANO FD2», que no sirven para la prueba.
Pagarpulsa el botónTe lleva a la pantalla del PSE de prueba (pasos abajo).

Pantalla del PSE de prueba (el sandbox de la pasarela pide confirmar el pago a mano):

  1. En el formulario elige PSE y el banco BANCO UNION COLOMBIANO y continúa al pago.
  2. En la pantalla del PSE de prueba pulsa Debug.
  3. bankProcessDate: la misma fecha que muestra la pantalla · transactionState: OK · authorizationID: 12.
  4. Pulsa Call: debe responder Call Return: SUCCESS - TransactionState: OK.
  5. Pulsa Return to PPE: vuelves al anchor y el USDC se acredita en tu wallet en pocos minutos.

Formulario de retiro, campo por campo

CampoQué escribirNota
Monto a retirar *1 USDC — o 1.11 para el retiro con devoluciónVerás la tasa y la comisión antes de confirmar.
Tu nombre completo (quien retira) *Tester SandboxEs la identidad de quien retira, no la del beneficiario: a ese lo identifica su llave, más abajo. Con una wallet externa el formulario lo dice así; desde el panel de VANK lo verás como «Nombre completo del titular».
Tipo de documento *CC · Cédula de CiudadaníaEs la primera opción de la lista.
Número de documento *1000000001Solo dígitos, sin puntos. Es la identidad de prueba con verificación preparada en el sandbox.
Correo electrónico *tester@example.comCualquiera con formato de correo.
Teléfono *3001234567Cualquier número.
Autorizo la verificación de identidad (KYC) *marca la casillaCorre sobre la cédula que escribiste. Es el primer chequeo; el segundo es al beneficiario, y lo hace la llave.
Cómo quieres recibir tu dinero *Llave Bre-BLa cuenta bancaria aparece como «próximamente»: no la uses.
Llave Bre-B del beneficiario *@alphamunKey01Solo la llave. El formulario resuelve al beneficiario de prueba y lo screenea solo: su nombre y documento NO se escriben. Los campos de arriba son de la persona que retira, no de él.
Confirmarpulsa el botónEnvía el USDC a la cuenta y memo que muestra (tu wallet suele hacerlo sola). Los pesos llegan en minutos; con 1.11 USDC verás el rechazo y el USDC de vuelta.

Por API (SEP-6 / SEP-12) — los mismos datos, con los nombres de campo del estándar:

bash
# SEP-6 · depósito — NO necesita SEP-12: la identidad se verifica en el formulario del more_info_url
GET /sep6/deposit?asset_code=USDC&account=G...&amount=2&type=bank_transfer
#   → paga por el more_info_url de la transacción (mismo formulario y mismo PSE de prueba)

# SEP-6 · retiro — SÍ necesita SEP-12: PUT /sep12/customer (Authorization: Bearer <JWT SEP-10>)
{
  "account": "G...TU_CUENTA_TESTNET",
  "type": "sep6-withdrawal",
  "first_name": "Tester",                     # quien retira: tu usuario
  "last_name": "Sandbox",
  "email_address": "tester@example.com",
  "id_type": "CC",
  "id_number": "1000000001",
  "bank_account_number": "@alphamunKey01"     # el beneficiario: solo su llave Bre-B
}
GET /sep6/withdraw?asset_code=USDC&account=G...&amount=1&type=breb   # amount=1.11 → con devolución
Al terminar, envía los 3 transaction IDs (depósito, retiro y retiro con devolución) a contacto@vank.co. Si algo del sandbox no responde como se describe aquí, escríbenos al mismo correo.

8 · Referencia

ProtocolosSEP-10 (autenticación) · SEP-24 (con formulario) · SEP-6 (por API) · SEP-12 (datos del cliente) · SEP-38 (tasa en firme)
ActivoUSDC en Stellar (emisor en el stellar.toml de cada ambiente; en producción, Circle)
Depósito mínimo5.000 COP
Retiro mínimo1 USDC
Entrada fiatPSE (Colombia)
Salida fiatBre-B — pagos inmediatos a cualquier llave (Colombia)
CotizacionesTasa final más la comisión declarada aparte, en firme y de un solo uso (SEP-38). 10 min en depósito, 5 en retiro
Soportecontacto@vank.co

9 · Métricas en vivo (mainnet)

Cada número de esta sección sale de la cadena: tu navegador lee los pagos en USDC de la cuenta de tesorería del anchor en la red pública de Stellar y los suma. No pasa por ningún servidor de VANK. Un depósito es USDC que la tesorería entrega a un usuario; un retiro es USDC que recibe de un usuario.

Leyendo la cadena…

Los protocolos SEP son estándares abiertos del ecosistema Stellar — la especificación completa vive en stellar.org. VANK · Powered by Stellar.