SEP-24SEP-6USDCStellarColombia · COP

VANK Stellar Anchor — Integration Guide

Connect your wallet or exchange to the Colombian peso. Your users convert USDC ⇄ COP with PSE pay-ins and Bre-B pay-outs — VANK handles compliance, KYC and settlement.

1 · What is the VANK Anchor?

An anchor is the bridge between the Stellar network and local money. The VANK Anchor connects USDC (the digital dollar issued by Circle) with the Colombian peso, using the standard Stellar ecosystem protocols: SEP-10 for authentication and SEP-24 for interactive deposits and withdrawals.

⬇️ Deposit (on-ramp) · COP → USDC

Your user pays pesos via PSE from their bank and receives USDC in their Stellar account. VANK collects the fiat, runs compliance and delivers the USDC on-chain.

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

Your user sends USDC to the anchor and instantly receives pesos at their Bre-B key — Colombia's instant payment system. The quote is shown before confirming, with no hidden spread.

Your integration speaks standard SEP-10 + SEP-24: if your wallet already connects to other ecosystem anchors (MoneyGram Ramps, for instance), connecting to VANK is the same code pointing at a different domain.

2 · Integration in 6 steps

  1. 1

    Start on testnet — no permission needed

    The sandbox is open: point your integration at dev-stable-anchor.thisisvank.com and start today. Writing to us comes later, to move to production.

  2. 2

    Set up your testnet wallet

    A Stellar testnet account with a trustline to test USDC. The issuer is in the test environment's stellar.toml.

  3. 3

    Implement SEP-10 and pick your path

    Authentication first (SEP-10). Then you pick one of two: with SEP-24 you open our form and we guide the user, which is a few lines with the Stellar Wallet SDK. With SEP-6 everything goes over the API and you own the screen. Code for both is below, and SEP-6 has its own section.

  4. 4

    Certify on testnet

    Three cases you run yourself: a deposit, a withdrawal and a refunded withdrawal. Section 7 has what each one proves and what we check.

  5. 5

    Approval

    VANK reviews your certification and use case, and coordinates the integration agreement with you.

  6. 6

    Production

    You provide your app's final domain and move to mainnet: anchor.thisisvank.com, with Circle's USDC.

3 · Initiating a transaction

SEP-1 · stellar.toml

Everything starts at the environment's stellar.toml: it holds EVERY endpoint you will use, the key the anchor signs with and the supported asset. Do not hardcode any of these values — read them from the toml, which is the source of truth and differs between environments.

toml fieldProtocolWhat you need it for
WEB_AUTH_ENDPOINTSEP-10Authenticate. It is the first step of everything else.
TRANSFER_SERVERSEP-6Deposit and withdrawal over the API, without our form. It is the endpoint of section 5. This field also tells you whether SEP-6 is enabled in that environment: if the toml publishes it, it is.
TRANSFER_SERVER_SEP0024SEP-24Deposit and withdrawal by opening our form.
KYC_SERVERSEP-12User identity. And on a SEP-6 withdrawal, this is where the destination Bre-B key travels.
ANCHOR_QUOTE_SERVERSEP-38Firm quotes, to guarantee the rate for your user.
SIGNING_KEYSEP-10The public key the anchor signs the challenge with. Verify it.
CURRENCIESSEP-1The supported USDC and its ISSUER, which differs between testnet and production.
Sandbox (testnet)Production (mainnet)
Anchor domaindev-stable-anchor.thisisvank.comanchor.thisisvank.com
stellar.toml/.well-known/stellar.toml/.well-known/stellar.toml
NetworkStellar testnetStellar mainnet
USDC issuertest issuer — read it from the tomlCircle
⚠️ The USDC issuer differs between environments. Always read it from the stellar.toml — never hardcode it.
curl
# Read the environment's stellar.toml — it is the source of truth
curl https://dev-stable-anchor.thisisvank.com/.well-known/stellar.toml   # production: anchor.thisisvank.com

SEP-10 · Authentication

SEP-10 proves you control the Stellar account: you request a challenge, sign it and exchange it for a JWT that authorizes the remaining calls. Watch this part, it is the one most often missed: the challenge is signed with TWO keys. The user account's key, which says who operates, and YOUR DOMAIN's key, which says which app originates it. The second one is an access requirement and is explained below.

curl
# Without the SDK, SEP-10 is two calls (any language):
GET  https://dev-stable-anchor.thisisvank.com/auth?account=G...        # → { transaction, network_passphrase }
#    Verify the challenge BEFORE signing: read_challenge_transaction(xdr, toml SIGNING_KEY, passphrase, home_domain, web_auth_domain) in your language's SDK
#    Sign it with the account key (and your domain key if you sent client_domain)
POST https://dev-stable-anchor.thisisvank.com/auth   {"transaction": "<xdr firmado>"}   # → { token }  (JWT, 24 h)
shell
yarn add @stellar/typescript-wallet-sdk
Your app's identity (client_domain) — OPTIONAL in the sandbox (you can authenticate with the account key alone and move on); required for production: your domain must publish its own stellar.toml at https://your-domain/.well-known/stellar.toml with a SIGNING_KEY, and your SEP-10 authentication must send that clientDomain (your backend co-signs the challenge with that key; the Wallet SDK supports this natively). It is the ecosystem-standard mechanism — the same one you use with other anchors — and it is how the anchor knows, with cryptographic proof rather than a claim, which wallet originates each transaction.
TypeScript
import { Wallet, SigningKeypair, DomainSigner } from "@stellar/typescript-wallet-sdk";

const HOME_DOMAIN = "dev-stable-anchor.thisisvank.com"; // sandbox · production: anchor.thisisvank.com
const CLIENT_DOMAIN = "tu-dominio.com";                 // yours, the one publishing your SIGNING_KEY

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

  // Your backend co-signs the challenge with your domain key.
  // The SECRET key never leaves it, and never reaches the browser.
  const walletSigner = new DomainSigner("https://" + CLIENT_DOMAIN + "/sign", {});

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

The /sign endpoint is yours and lives on your backend. It receives the challenge, signs it with your domain's secret key and returns it. That secret key never leaves your server and never reaches the browser — which is why the backend signs and not the client:

TypeScript
// On YOUR backend. It receives the challenge, signs it with your domain key and returns it.
// 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!)); // your SIGNING_KEY's secret
  res.json({ transaction: tx.toXDR(), network_passphrase });
});

The file is minimal — this is ALL it needs to contain:

toml
# https://your-domain/.well-known/stellar.toml
VERSION = "2.7.0"
SIGNING_KEY = "G...YOUR_APPS_PUBLIC_KEY"
  • SIGNING_KEY is the PUBLIC key of a Stellar keypair your team controls. The secret key is never published: it is what your backend uses to co-sign the SEP-10 challenge.
  • Generate the keypair with the SDK or the Stellar Lab. The account needs no funds and does not need to exist on-chain — it only identifies your app.
  • It must respond over public https, at exactly that path, with no authentication and open CORS (Access-Control-Allow-Origin: *). The anchor reads it on every authentication.
  • Does your app already publish a stellar.toml for other reasons? Great: just make sure it includes SIGNING_KEY — no other field is needed to connect to VANK.

SEP-24 · Interactive deposit & withdrawal

The call returns a URL and an id. You open the URL in a webview and there the user sees the full breakdown before confirming: the final rate, the commission and VAT SEPARATELY, the net total they will receive, and a countdown with the real time left on that rate. Nothing is buried inside the price. The id is your handle to poll the status.

TypeScript
// Withdrawal (off-ramp): the user sends USDC and receives COP
const { url, id } = await anchor.sep24().withdraw({
  authToken,
  withdrawalAccount: USER_STELLAR_PUBLIC_KEY,
  assetCode: "USDC",
  lang: "en",
});
// Open `url` in a webview: VANK's UI guides the rest
TypeScript
// Deposit (on-ramp): the user pays COP (PSE) and receives USDC
const { url, id } = await anchor.sep24().deposit({
  authToken,
  destinationAccount: USER_STELLAR_PUBLIC_KEY,
  assetCode: "USDC",
  lang: "en",
});
The link must be opened within 10 minutes of requesting it: that is how long its token lives. Once opened, the form keeps working even if the token expires — the limit is opening it, not completing it. And a transaction stuck in “incomplete” is a form that was opened and never submitted: do not treat it as an in-flight operation.

4 · Statuses and your action at each one

Track the status with the SDK watcher (recommended) or the raw endpoint. There are no webhooks to your URL: the on_change_callback parameter is accepted but never fires, so poll. A 403 only says forbidden: the JWT is missing, invalid or expired (it lasts 24 h); redo SEP-10. The lifecycle is standard 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") {
      // Withdrawal: send the USDC now. Deposit: wait for the user's payment
    }
  },
  onSuccess: (tx) => { /* completed */ },
  onError: (tx) => { /* error */ },
});
curl
# Without the SDK: start and poll (sandbox; production: 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"}'      # or /withdraw/interactive → { url, id }

curl "https://dev-stable-anchor.thisisvank.com/sep24/transaction?id=$TRANSACTION_ID" \
  -H "Authorization: Bearer $SEP10_JWT"
StatusWhat it meansYour action
incompleteThe form was opened and not yet submitted.Nothing. It does NOT expire on its own: it stays there indefinitely. If your user abandoned it, start another — and to resume it you must request the link again.
pending_user_transfer_startWithdrawal: the anchor awaits your USDC. Deposit: awaiting the user's PSE payment.Withdrawal: send the USDC to the account and memo the transaction provides. Deposit: nothing.
pending_anchorVANK received the funds and is processing.Nothing — keep watching.
pending_externalWithdrawal: the pesos are on their way via Bre-B.Nothing — usually seconds. If the provider fails, the transaction moves to error and the refund goes out on its own.
completedDelivered: COP at the user's account or USDC in their wallet.Show the receipt (more_info_url).
errorThe operation could not be completed.Show the reason; if USDC was received, the refund is automatic.

Refunds

If a withdrawal cannot be completed after the USDC is received, the refund to the source account is automatic, with no human intervention, and for the full amount: the service was not delivered, so no fee is deducted.

Compliance

Every operation goes through identity verification and beneficiary screening (restricted lists, PEP, OFAC) before money moves. The control is fail-closed: if screening cannot run, the operation does not proceed.

5 · SEP-6: the API path

Everything above uses SEP-24: your app opens our form and we guide the user. With SEP-6 your app requests, asks and confirms over the API, and you own the screen. The WITHDRAWAL is entirely programmatic: nothing is ever opened. The DEPOSIT ends in a link, and not by our choice: the peso payment goes through PSE, where the person picks their bank, so no payment URL exists before that choice. Below is exactly where that link comes from.

⚠️ SEP-6 is enabled per environment, and the stellar.toml itself tells you without asking us: if it publishes TRANSFER_SERVER, SEP-6 is available there; if it does not, not yet. The sandbox already serves it. Check the toml before pointing your integration at a new environment — it is the same rule you already apply to the USDC issuer. SEP-24 is available everywhere.
The most confusing part when integrating in SEP-6 the withdrawal request does NOT carry the destination. Where the money goes travels separately, via SEP-12, in the standard bank_account_number field, and we ask for it on EVERY withdrawal: we never reuse the previous one. Send it with transaction_id; if you send it without one, the withdrawal of that account waiting for a key takes it. And mind who is who in that SEP-12: the identity fields (name, document, email) belong to YOUR USER, the one withdrawing, just as in SEP-24 they type them into our form. For the BENEFICIARY you only send the key: we resolve their name and document against the Bre-B directory and screen them separately. We hand them back to you in message so you can show them to your user before signing. Two checks on two people, on purpose, the sender and the receiver: neither may have legal problems.

What to install

shell
npm install @stellar/stellar-sdk        # sign SEP-10 and the on-chain payment
# Nothing else: the rest of SEP-6 is plain HTTP (fetch).

A withdrawal, step by step

Authenticate first with SEP-10, same as step 3 of this guide. The rest is plain HTTP:

TypeScript
const ANCHOR = "https://dev-stable-anchor.thisisvank.com";   // SEP-6 lives TODAY in the sandbox
const auth = { Authorization: `Bearer ${authToken}` };   // SEP-10 JWT (step 3)

// 1) Request the withdrawal. Returns an id; you cannot send the USDC yet.
const { id } = await fetch(
  `${ANCHOR}/sep6/withdraw?asset_code=USDC&account=${account}&amount=2&type=breb`,
  { headers: auth },
).then((r) => r.json());

// 2) Poll the transaction and REACT to its status.
const tx = async () =>
  (await fetch(`${ANCHOR}/sep6/transaction?id=${id}`, { headers: auth }).then((r) => r.json()))
    .transaction;

// Right after creation it is incomplete: wait for the engine's FIRST cycle (up to ~30s) before deciding anything. Ask only once now and you skip the next step.
let t = await tx();
while (t.status === "incomplete") { await sleep(5000); t = await tx(); }
if (t.status === "pending_customer_info_update") {
  // 3) Data is missing. ASK which — never guess.
  const need = await fetch(
    `${ANCHOR}/sep12/customer?account=${account}&transaction_id=${id}`,
    { headers: auth },
  ).then((r) => r.json());
  console.log(need.fields);   // each field carries a description for your user

  // 4) Send them BOUND to this transaction. bank_account_number = the Bre-B key.
  await fetch(`${ANCHOR}/sep12/customer`, {
    method: "PUT",
    headers: { ...auth, "Content-Type": "application/json" },
    body: JSON.stringify({
      account,
      transaction_id: id,
      type: "sep6-withdrawal",   // withdrawal context: this is how SEP-12 knows the key is required
      // YOUR USER, the one withdrawing. Same as what they type into our form in SEP-24.
      first_name: "Ana", last_name: "Pérez",
      email_address: "ana@example.com",
      id_type: "CC", id_number: "1000000001",
      // THE BENEFICIARY: only their key. You do NOT send their name or document — we resolve them against the Bre-B directory.
      bank_account_number: "@llaveDeTuUsuario",
    }),
  });
}

// 5) POLL — do not expect an immediate answer. After the PUT, the transaction
//    can take up to ~30s to advance: our engine works in cycles.
while (["incomplete", "pending_customer_info_update"].includes((t = await tx()).status)) await sleep(5000);
// Once it reaches pending_user_transfer_start you have the destination and memo.
// t.withdraw_anchor_account · t.withdraw_memo (memo_type "id") · t.amount_out = net COP
// t.fee_details.total comes in USDC: it is the same fee the quote gave you in COP, in another unit
// t.message = the resolved beneficiary: show it to the user BEFORE signing

// 6) Send the USDC to that account with THAT memo, then keep polling until completed.
Poll, do not assume. None of these calls changes the status instantly: our engine reviews transactions in cycles, so after sending the customer data the transaction can take up to about 30 seconds to move. If your app concludes it failed because the status did not change right away, you will show an error to someone whose withdrawal is going through just fine.

The statuses you will see

StatusWhat it means and what you do
incompleteWITHDRAWAL: just created; we are calculating and verifying, it moves on the next cycle, up to ~30s. DEPOSIT: it STAYS here, and that is correct — we are waiting for your user to pay through the link. Do not expect it to move on its own.
pending_customer_info_updateWe need customer data. Query GET /sep12/customer with this transaction_id, show the fields to your user and send them with PUT. The transaction's message field tells you what is missing.
pending_user_transfer_startAll set. Send the USDC to withdraw_anchor_account with withdraw_memo. Before signing, show the beneficiary that comes in message.
pending_anchor · pending_externalWe received your USDC and the peso payout is under way. Just wait.
completedThe beneficiary received the pesos.
errorIt could not continue and message says why. If you had already sent the USDC, the refund is automatic and for the full amount.

The deposit

There are two ways to start a deposit, and they do not lock the same things:

Endpointamount is inWhat gets lockedWhen to use it
GET /sep6/depositUSDCNothing. It is an intention: the form opens with the amount editable, your user types the pesos they will pay and it settles at the current rate.When you do not need to promise your user a number. Careful: amount=5000 here is five thousand dollars.
GET /sep6/deposit-exchange + quote_idCOPAmount and rate. The form opens with both locked, and what was promised is what arrives.When you guarantee the rate to your user. It is the one in the code below.

Both return the same thing, and it is what confuses most: only an id and a fixed sentence telling you to check the transaction. The link is not there. It lives in the transaction, in the more_info_url field, already signed. You open it in a webview, your user picks their bank and pays through PSE, and once the payment is confirmed the USDC lands in their Stellar account and the transaction moves to completed. Recommended minimum: 2,000 COP, because the deposit fee is 1,785 and it is deducted before converting.

TypeScript
// 1) Firm quote (optional, but it is what guarantees the rate for your user)
// USDC_ISSUER: the issuer you read from the environment's stellar.toml (section 3). Never hardcoded.
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  ← the fee comes SEPARATELY, not inside the price

// 2) Start the deposit with that quote.
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());
// CAREFUL: this response does NOT carry the link. Only { how, id }.

// 3) The link lives in the TRANSACTION.
const t = await fetch(`${ANCHOR}/sep6/transaction?id=${id}`, { headers: auth })
  .then((r) => r.json()).then((r) => r.transaction);
// t.more_info_url            → open it in a webview. Its token lives 10 min; if it expires, re-read and you get a new one.
// t.user_action_required_by  → how long your user has. TODAY it comes null: use the quote's expires_at

// 4) Your user picks a bank and pays through PSE. Keep polling until completed.

After the link: what happens and what you see

Your app opened the more_info_url in a webview and keeps polling GET /sep6/transaction. This is what your user does at each moment and the status your polling will see meanwhile.

MomentStatus your polling seesWhat is happening
Opens the linkincompleteSees the locked amount, the frozen rate, commission and VAT separately and the net USDC, with the real countdown. Fills in their details, authorizes KYC (it runs on the ID number they type), picks their bank and presses Continue.
Submitted the formpending_user_transfer_startWe created the order and the quote is consumed: the 10-minute window ends here. We send them to their bank's portal (PSE). That payment may take as long as it takes.
The bank confirmedpending_anchorWe received the pesos and are sending the USDC to their Stellar account.
Donecompletedstellar_transaction_id carries the hash and message says “USDC enviado on-chain”. Your user sees “Pago confirmado” with that same hash. Show it to them.
Abandoned itincompleteIt stays there, it does not expire on its own. If they want to pay later, start another deposit: the link and the quote have expired.

Locked price before moving money

Every SEP-6 operation comes in two versions: the plain one and the -exchange one. The difference is one single thing, whether the rate is locked before money moves or not:

Without a quoteWith a firm quote
Endpoints/sep6/deposit · /sep6/withdrawFirst POST /sep38/quote, which gives you a quote_id. Then /sep6/deposit-exchange or /sep6/withdraw-exchange with that quote_id.
The rateThe one at settlement time. It can move between requesting the operation and completing it.The quote's, frozen for that transaction. Single use, it does not work for another one.
What you can promise your userNothing exact. They see the number at the end.The exact number before they confirm: the quote carries the price and the fee separately, and what was promised is what arrives.
When to use itWhen your app does not show an amount before operating.When your app tells them “you will receive X” before they pay or send. It is what any serious app does.
How long it lastsNot applicable.10 minutes on deposits, because the person opens the link, verifies and picks a bank. 5 minutes on withdrawals, which are programmatic.

What has to happen within the window: on a deposit, your user submits the form; the PSE payment may take longer. On a withdrawal, you send the key and the data via SEP-12; our engine consumes the quote when it creates the order.

How to test it

With the test data in section 7, against the sandbox. Two things worth checking there, because they are the ones that surprise in production: make TWO withdrawals in a row and confirm we ask for the key again on the second, and make one of 1.11 USDC to see the automatic refund. If you test the DEPOSIT with the SDF Demo Wallet, note that this tool receives the more_info_url and does not show it to you: it only logs the transaction status. Take the POST /auth token and the deposit id from its logs, call GET /sep6/transaction?id=… with that token, and the link is right there. Once you pay, the Demo Wallet picks up the completed status and closes on its own.

bash
# SEP-6 · deposit — does NOT need SEP-12: identity is verified in the more_info_url form
GET /sep6/deposit?asset_code=USDC&account=G...&amount=2&type=bank_transfer
#   → pay through the transaction's more_info_url (same form, same test PSE)

# SEP-6 · withdrawal — DOES need SEP-12: PUT /sep12/customer (Authorization: Bearer <SEP-10 JWT>)
{
  "account": "G...YOUR_TESTNET_ACCOUNT",
  "type": "sep6-withdrawal",
  "first_name": "Tester",                     # who withdraws: your user
  "last_name": "Sandbox",
  "email_address": "tester@example.com",
  "id_type": "CC",
  "id_number": "1000000001",
  "bank_account_number": "@alphamunKey01"     # the beneficiary: only their Bre-B key
}
GET /sep6/withdraw?asset_code=USDC&account=G...&amount=1&type=breb   # amount=1.11 → refunded

6 · Not managing Stellar keys? Custodial accounts

If your product is not a Stellar wallet, VANK can also create and custody a Stellar account for each of your users: you reference it with your own identifier and never touch private keys — VANK manages them securely. It is the fast path for fintechs that want to offer USDC without running crypto infrastructure.

Access to the custodial integration follows the same request and certification process in this guide — write to us and we will share the specific technical documentation.

7 · Certification and going live

Certification is not paperwork you wait on: you run it yourself, today, in the open sandbox. Three cases, each proving your integration handles a different moment well. Here is what to run and, above all, what we will look at — so you know whether you will pass before sending it.

CaseWhat it provesWhat we check
1 · A depositThat you take the user to pay and credit the USDC to their account.That the USDC reached the account you declared, and that your app showed the peso amount before the person paid.
2 · A withdrawalThat you ask for the destination, send the USDC to the right place and follow the operation to the end.That you showed the user the resolved beneficiary BEFORE signing, and that the net amount you displayed matches what we paid.
3 · A refunded withdrawalThat you handle the case where the payout fails. Withdraw exactly 1.11 USDC: the sandbox rejects it on purpose.That you reacted to the error status, explained it to the user with our message, and reflected the returned USDC instead of writing it off.
Once you have them, send the three transaction IDs to contacto@vank.co. We review each one against the above and reply with what we found: if something fails we tell you exactly what, not a generic rejection.

Then: going live

  1. Create your VANK account (app.vank.co) and write to contacto@vank.co with your app's domain, what you are building and your use case.
  2. Requirement to connect: your domain must publish the public file https://tu-dominio/.well-known/stellar.toml with your app's SIGNING_KEY, and your SEP-10 authentication must send that clientDomain (co-signing the challenge — the Wallet SDK supports it). It is the ecosystem standard and it is what lets us attribute your transactions to you.
  3. We register your domain, sign the integration agreement, and you go live on mainnet with Circle's USDC.

Sandbox test data

Everything you need to complete certification without waiting for us. This data is fictitious and shared: there is no real person or money behind it, and you must not use real documents in the sandbox.

WhatValueNote
Environmentdev-stable-anchor.thisisvank.comStellar testnet. Use your own testnet wallet (or the SDF Demo Wallet).
Testnet fundsXLM: Friendbot · USDC: trustline to the issuer in the stellar.toml, then request it at https://faucet.circle.com (Stellar testnet)Trustline first, then Circle's faucet: without the trustline the USDC cannot land. Your own test deposit also leaves you USDC.
KYC identityTester Sandbox · CC 1000000001Fictitious identity, verified instantly. Email and phone: anything.
Deposit (test PSE)≥ 5.000 COP · bank BANCO UNION COLOMBIANO · the min_amount in /info is in USDCWith less, the sandbox's fixed fee eats the amount. Follow the test-PSE steps below.
Withdrawal (test Bre-B)key @alphamunKey01 · ≥ 1 USDCTest holder in the sandbox's Bre-B directory. We ask for it on EVERY withdrawal: the destination is never inherited from the previous one.
Refunded withdrawalsame key · exactly 1.11 USDCThe sandbox rejects that amount on purpose: you will see the transaction in error and the USDC back in your account, automatically.
Via API (SEP-6 / SEP-12)first_name=Tester last_name=Sandbox id_type=CC id_number=1000000001For the withdrawal, the Bre-B key goes in the field bank_account_number.

Deposit form, field by field (every field marked * is required; type exactly this):

FieldWhat to typeNote
Amount to deposit *5000In COP. Any less and the sandbox's fixed fee eats it.
Full name *Tester Sandbox
Document type *CC · Cédula de CiudadaníaIt is the first option in the list.
Document number *1000000001Digits only, no dots.
Email *tester@example.comAnything in email format.
Phone *3001234567Any number.
I authorize the identity verification (KYC) *tick the boxWithin seconds you see “Identity verified · KYC complete”.
Payment method *PSE · bank BANCO UNION COLOMBIANOExactly that one: the list also has “BANCO UNION” and “BANCO UNION COLOMBIANO FD2”, which do not work for the test.
Paypress the buttonTakes you to the test-PSE screen (steps below).

Test-PSE screen (the gateway's sandbox asks you to confirm the payment manually):

  1. In the form choose PSE and the bank BANCO UNION COLOMBIANO and continue to payment.
  2. On the test-PSE screen press Debug.
  3. bankProcessDate: the same date the screen shows · transactionState: OK · authorizationID: 12.
  4. Press Call: it must answer Call Return: SUCCESS - TransactionState: OK.
  5. Press Return to PPE: you return to the anchor and the USDC lands in your wallet within a few minutes.

Withdrawal form, field by field

FieldWhat to typeNote
Amount to withdraw *1 USDC — or 1.11 for the refunded withdrawalYou see the rate and the fee before confirming.
Your full name (the person withdrawing) *Tester SandboxThis is the identity of the person withdrawing, not the beneficiary's: the key below identifies them. With an external wallet the form says it this way; from the VANK panel you will see it as “Account holder full name”.
Document type *CC · Cédula de CiudadaníaIt is the first option in the list.
Document number *1000000001Digits only, no dots. It is the test identity with verification prepared in the sandbox.
Email *tester@example.comAnything in email format.
Phone *3001234567Any number.
I authorize the identity verification (KYC) *tick the boxIt runs on the ID number you typed. It is the first check; the second is on the beneficiary, and the key handles it.
How you want to receive your money *Bre-B keyBank account shows as “coming soon”: do not use it.
Beneficiary's Bre-B key *@alphamunKey01Only the key. The form resolves the test beneficiary and screens them by itself: their name and document are NOT typed. The fields above belong to the person withdrawing, not to them.
Confirmpress the buttonSend the USDC to the account and memo shown (your wallet usually does it for you). Pesos arrive within minutes; with 1.11 USDC you will see the rejection and the USDC back.

Via API (SEP-6 / SEP-12) — the same data, with the standard field names:

bash
# SEP-6 · deposit — does NOT need SEP-12: identity is verified in the more_info_url form
GET /sep6/deposit?asset_code=USDC&account=G...&amount=2&type=bank_transfer
#   → pay through the transaction's more_info_url (same form, same test PSE)

# SEP-6 · withdrawal — DOES need SEP-12: PUT /sep12/customer (Authorization: Bearer <SEP-10 JWT>)
{
  "account": "G...YOUR_TESTNET_ACCOUNT",
  "type": "sep6-withdrawal",
  "first_name": "Tester",                     # who withdraws: your user
  "last_name": "Sandbox",
  "email_address": "tester@example.com",
  "id_type": "CC",
  "id_number": "1000000001",
  "bank_account_number": "@alphamunKey01"     # the beneficiary: only their Bre-B key
}
GET /sep6/withdraw?asset_code=USDC&account=G...&amount=1&type=breb   # amount=1.11 → refunded
When done, send the 3 transaction IDs (deposit, withdrawal and refunded withdrawal) to contacto@vank.co. If anything in the sandbox does not behave as described here, write to the same address.

8 · Reference

ProtocolsSEP-10 (authentication) · SEP-24 (with form) · SEP-6 (over API) · SEP-12 (customer data) · SEP-38 (firm rate)
AssetUSDC on Stellar (issuer in each environment's stellar.toml; Circle in production)
Minimum deposit5.000 COP
Minimum withdrawal1 USDC
Fiat pay-inPSE (Colombia)
Fiat pay-outBre-B — instant payments to any key (Colombia)
QuotesFinal rate plus the fee declared separately, firm and single-use (SEP-38). 10 min on deposits, 5 on withdrawals
Supportcontacto@vank.co

9 · Live metrics (mainnet)

Every number in this section comes from the chain: your browser reads the USDC payments of the anchor's treasury account on the Stellar public network and adds them up. No VANK server is involved. A deposit is USDC the treasury delivers to a user; a withdrawal is USDC it receives from a user.

Reading the chain…

SEP protocols are open standards of the Stellar ecosystem — the full specification lives at stellar.org. VANK · Powered by Stellar.