SSeev PlusDocs
Stellar Anchor

SEP-12: KYC

Submit and manage customer verification data for the Ghana corridor using the SEP-12 KYC API.

SEP-12 defines a standard API for submitting Know Your Customer (KYC) data. Before a customer can deposit or withdraw, the anchor must verify their identity. KYC data is submitted once and linked to the authenticated Stellar account.

Endpoints

MethodPathDescription
GET/sep12/customerGet current KYC status and required fields
PUT/sep12/customerSubmit or update KYC data
DELETE/sep12/customerDelete customer KYC record

All endpoints require a valid SEP-10 JWT token.

KYC status lifecycle

┌──────────┐     PUT /customer      ┌────────────────┐
│  NEEDS   │ ──────────────────────►│   PROCESSING   │
│  DATA    │                         │  (under review)│
└──────────┘                         └───────┬────────┘

                              ┌──────────────┼──────────────┐
                              ▼              ▼              ▼
                       ┌──────────┐  ┌──────────────┐ ┌──────────┐
                       │ ACCEPTED │  │ NEEDS_INFO   │ │ REJECTED │
                       └──────────┘  │ (more fields)│ └──────────┘
                                     └──────────────┘
StatusMeaning
NEEDS_DATANo KYC submitted yet. Submit required fields.
PROCESSINGKYC submitted, under review.
ACCEPTEDKYC approved. Customer can transact.
NEEDS_INFOAdditional information required. Check fields for what's needed.
REJECTEDKYC rejected. Customer cannot transact.

Required fields for Ghana corridor

Individual customers (deposit/withdrawal)

Field (SEP-9)TypeDescriptionRequired
first_namestringCustomer's first nameYes
last_namestringCustomer's last nameYes
email_addressstringEmail addressYes
mobile_numberstringPhone number (e.g., +233241234567)Yes
mobile_money_numberstringMobile money wallet number (E.164 format)Yes (for mobile funding)
mobile_money_providerstringMoMo provider: MTN, VODAFONE, TELECEL, AIRTELTIGOYes (for mobile funding)
id_typestringType of ID: ghana_card, passport, voter_idYes
id_numberstringID document numberYes
date_of_birthstringDate of birth (YYYY-MM-DD)Yes
addressstringResidential addressNo

The mobile_number must include the country code (+233). This number is used for Mobile Money collection and disbursement.

Mobile money fields for SEP-6

When using SEP-6 deposits or withdrawals with funding_method=mobile, the anchor requires additional mobile money-specific fields. These are requested when you call GET /sep12/customer with a transaction_id from a SEP-6 deposit/withdrawal.

Required mobile money fields

Field (SEP-9)TypeDescriptionExample
mobile_money_numberstringMobile money wallet number (E.164 format)+233244123456
mobile_money_providerstringMobile money network providerMTN

Supported providers

ProviderValuePhone prefixes
MTN Mobile MoneyMTN024, 025, 053, 054, 055, 059
Vodafone CashVODAFONE020, 050
Telecel CashTELECEL026, 027, 056, 057
AirtelTigo MoneyAIRTELTIGO026, 027, 056, 057

mobile_money_number and mobile_money_provider are separate from the basic mobile_number field. The anchor uses mobile_money_number as the source for collections (deposits) and destination for payouts (withdrawals). If only mobile_number is provided, the anchor falls back to it.

How mobile money fields connect to SEP-6

When you initiate a SEP-6 deposit with funding_method=mobile:

  1. If mobile money details exist in KYC — the anchor triggers collection immediately (auto mode)
  2. If mobile money details are missing — transaction goes to pending_customer_info_update

To resolve pending_customer_info_update:

# 1. Check what fields are needed (pass transaction_id)
curl -X GET "https://<your-anchor-domain>/sep12/customer?account=GUSER...&transaction_id=txn-abc-123" \
  -H "Authorization: Bearer <sep10_jwt>"
import { Wallet, Keypair } from '@stellar/typescript-wallet-sdk';

const wallet = Wallet.TestNet();
const anchor = wallet.anchor({ homeDomain: '<your-anchor-domain>' });
const accountKp = Keypair.fromSecret('SCZANGBA5YHTNYVVV3C7CAZMCLXPJLNS2YFGXDNASLFTGBPFMRKCY6ML');

// Authenticate first
const sep10 = await anchor.sep10();
const authToken = await sep10.authenticate({ accountKp });

// 1. Check what fields are needed
const sep12 = await anchor.sep12(authToken);
const customer = await sep12.getCustomer({ transactionId: 'txn-abc-123' });
console.log("Missing fields:", Object.keys(customer.fields));

Response will include the missing mobile money fields:

{
  "id": "customer-uuid",
  "status": "NEEDS_INFO",
  "fields": {
    "mobile_money_number": {
      "type": "string",
      "description": "Mobile money phone number (E.164 format, e.g. +233244123456)"
    },
    "mobile_money_provider": {
      "type": "string",
      "description": "Mobile money service provider",
      "choices": ["MTN", "VODAFONE", "TELECEL", "AIRTELTIGO"]
    }
  }
}
# 2. Submit the mobile money details
curl -X PUT "https://<your-anchor-domain>/sep12/customer" \
  -H "Authorization: Bearer <sep10_jwt>" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "GUSER...",
    "transaction_id": "txn-abc-123",
    "mobile_money_number": "+233244123456",
    "mobile_money_provider": "MTN"
  }'
import { Wallet, Keypair } from '@stellar/typescript-wallet-sdk';

const wallet = Wallet.TestNet();
const anchor = wallet.anchor({ homeDomain: '<your-anchor-domain>' });
const accountKp = Keypair.fromSecret('SCZANGBA5YHTNYVVV3C7CAZMCLXPJLNS2YFGXDNASLFTGBPFMRKCY6ML');

const sep10 = await anchor.sep10();
const authToken = await sep10.authenticate({ accountKp });

// 2. Submit mobile money details
const sep12 = await anchor.sep12(authToken);
const result = await sep12.add({
  sep9Info: {
    mobile_money_number: '+233244123456',
    mobile_money_provider: 'MTN',
  },
});
console.log("Status:", result.status); // "ACCEPTED"

Response:

{
  "id": "customer-uuid",
  "status": "ACCEPTED"
}

After this, the anchor automatically:

  • Advances the transaction to pending_user_transfer_start
  • Triggers the mobile money collection (deposit) or payout (withdrawal)

Complete KYC submission with mobile money

For the best experience, submit all fields including mobile money details upfront:

curl -X PUT "https://<your-anchor-domain>/sep12/customer" \
  -H "Authorization: Bearer <sep10_jwt>" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "GA2E5D24D2LCGVA3SVVKDP64OTN2M2NLUWRDYINB352HW35ABZ66MPVR",
    "first_name": "Patrick",
    "last_name": "Oduro",
    "email_address": "patrick@example.com",
    "mobile_number": "+233542853417",
    "mobile_money_number": "+233542853417",
    "mobile_money_provider": "MTN",
    "id_type": "ghana_card",
    "id_number": "GHA-123456789-0",
    "date_of_birth": "1992-03-20"
  }'
import { Wallet, Keypair } from '@stellar/typescript-wallet-sdk';

const wallet = Wallet.TestNet();
const anchor = wallet.anchor({ homeDomain: '<your-anchor-domain>' });
const accountKp = Keypair.fromSecret('SCZANGBA5YHTNYVVV3C7CAZMCLXPJLNS2YFGXDNASLFTGBPFMRKCY6ML');

const sep10 = await anchor.sep10();
const authToken = await sep10.authenticate({ accountKp });

async function submitFullKyc() {
  const sep12 = await anchor.sep12(authToken);
  const result = await sep12.add({
    sep9Info: {
      first_name: 'Patrick',
      last_name: 'Oduro',
      email_address: 'patrick@example.com',
      mobile_number: '+233542853417',
      mobile_money_number: '+233542853417',
      mobile_money_provider: 'MTN',
      id_type: 'ghana_card',
      id_number: 'GHA-123456789-0',
      date_of_birth: '1992-03-20',
    },
  });
  console.log("KYC Status:", result.status); // "ACCEPTED"
  return result;
}

Submitting mobile_money_number and mobile_money_provider during initial KYC means the SEP-6 deposit flow can trigger collection immediately without going through pending_customer_info_update.

Step 1: Check KYC status

curl -X GET "https://<your-anchor-domain>/sep12/customer?account=GCEXAMPLE4KEYPAIR7HERE2REPLACE5WITH5YOUR5ACTUAL5STELLAR5KEY" \
  -H "Authorization: Bearer <sep10_jwt_token>"
import { Wallet, Keypair } from '@stellar/typescript-wallet-sdk';

const wallet = Wallet.TestNet();
const anchor = wallet.anchor({ homeDomain: '<your-anchor-domain>' });
const accountKp = Keypair.fromSecret('SCZANGBA5YHTNYVVV3C7CAZMCLXPJLNS2YFGXDNASLFTGBPFMRKCY6ML');

const sep10 = await anchor.sep10();
const authToken = await sep10.authenticate({ accountKp });

const sep12 = await anchor.sep12(authToken);
const status = await sep12.getCustomer({});
console.log("KYC Status:", status.status);
console.log("Required fields:", Object.keys(status.fields || {}));

Response (new customer):

{
  "id": "d1c3e245-8a67-4e3c-b12f-1a2b3c4d5e6f",
  "status": "NEEDS_DATA",
  "fields": {
    "first_name": {
      "description": "Customer's first name",
      "type": "string",
      "optional": false
    },
    "last_name": {
      "description": "Customer's last name",
      "type": "string",
      "optional": false
    },
    "email_address": {
      "description": "Email address",
      "type": "string",
      "optional": false
    },
    "mobile_number": {
      "description": "Mobile money number with country code",
      "type": "string",
      "optional": false
    },
    "id_type": {
      "description": "Type of ID document",
      "type": "string",
      "choices": ["ghana_card", "passport", "voter_id"],
      "optional": false
    },
    "id_number": {
      "description": "ID document number",
      "type": "string",
      "optional": false
    },
    "date_of_birth": {
      "description": "Date of birth (YYYY-MM-DD)",
      "type": "string",
      "optional": false
    }
  }
}

Step 2: Submit KYC data

curl -X PUT "https://<your-anchor-domain>/sep12/customer" \
  -H "Authorization: Bearer <sep10_jwt_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "GCEXAMPLE4KEYPAIR7HERE2REPLACE5WITH5YOUR5ACTUAL5STELLAR5KEY",
    "first_name": "Kwame",
    "last_name": "Asante",
    "email_address": "kwame@example.com",
    "mobile_number": "+233241234567",
    "id_type": "ghana_card",
    "id_number": "GHA-123456789-0",
    "date_of_birth": "1990-05-15"
  }'
import { Wallet, Keypair } from '@stellar/typescript-wallet-sdk';

const wallet = Wallet.TestNet();
const anchor = wallet.anchor({ homeDomain: '<your-anchor-domain>' });
const accountKp = Keypair.fromSecret('SCZANGBA5YHTNYVVV3C7CAZMCLXPJLNS2YFGXDNASLFTGBPFMRKCY6ML');

const sep10 = await anchor.sep10();
const authToken = await sep10.authenticate({ accountKp });

const sep12 = await anchor.sep12(authToken);
const result = await sep12.add({
  sep9Info: {
    first_name: 'Kwame',
    last_name: 'Asante',
    email_address: 'kwame@example.com',
    mobile_number: '+233241234567',
    mobile_money_number: '+233241234567',
    mobile_money_provider: 'MTN',
    id_type: 'ghana_card',
    id_number: 'GHA-123456789-0',
    date_of_birth: '1990-05-15',
  },
});
console.log("KYC status:", result.status); // "ACCEPTED"

Response (200):

{
  "id": "d1c3e245-8a67-4e3c-b12f-1a2b3c4d5e6f",
  "status": "ACCEPTED"
}

For sandbox/testnet, KYC is typically auto-approved immediately. In production, there may be a PROCESSING state while the anchor reviews submissions.

TypeScript example

import { Wallet, Keypair } from '@stellar/typescript-wallet-sdk';

const wallet = Wallet.TestNet();
const ANCHOR_HOME_DOMAIN = '<your-anchor-domain>';
const anchor = wallet.anchor({ homeDomain: ANCHOR_HOME_DOMAIN });
const accountKp = Keypair.fromSecret('SCZANGBA5YHTNYVVV3C7CAZMCLXPJLNS2YFGXDNASLFTGBPFMRKCY6ML');

async function submitKyc() {
  // Authenticate
  const sep10 = await anchor.sep10();
  const authToken = await sep10.authenticate({ accountKp });

  // Submit KYC
  const sep12 = await anchor.sep12(authToken);
  const result = await sep12.add({
    sep9Info: {
      first_name: 'Kwame',
      last_name: 'Asante',
      email_address: 'kwame@example.com',
      mobile_number: '+233241234567',
      mobile_money_number: '+233241234567',
      mobile_money_provider: 'MTN',
      id_type: 'ghana_card',
      id_number: 'GHA-123456789-0',
      date_of_birth: '1990-05-15',
    },
  });

  console.log("KYC status:", result.status); // "ACCEPTED"
  return result;
}

async function getKycStatus() {
  const sep10 = await anchor.sep10();
  const authToken = await sep10.authenticate({ accountKp });

  const sep12 = await anchor.sep12(authToken);
  return sep12.getCustomer({});
}

// Usage
const kycResult = await submitKyc();
console.log("KYC status:", kycResult.status); // "ACCEPTED"

Handling NEEDS_INFO

If the anchor needs additional information, the response will indicate which fields are missing:

{
  "id": "d1c3e245-8a67-4e3c-b12f-1a2b3c4d5e6f",
  "status": "NEEDS_INFO",
  "fields": {
    "address": {
      "description": "Residential address is required for this transaction amount",
      "type": "string",
      "optional": false
    }
  },
  "message": "Additional information required for transactions above GHS 5,000"
}

Re-submit with the additional fields using the same PUT endpoint.

Delete customer data

To remove all KYC data associated with an account:

curl -X DELETE "https://<your-anchor-domain>/sep12/customer" \
  -H "Authorization: Bearer <sep10_jwt_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "GCEXAMPLE4KEYPAIR7HERE2REPLACE5WITH5YOUR5ACTUAL5STELLAR5KEY"
  }'

Mobile number format

NetworkExampleFormat
MTN+233241234567+233 + 9 digits
Vodafone+233201234567+233 + 9 digits
AirtelTigo+233261234567+233 + 9 digits

The mobile number submitted in KYC is used as the default payout destination for withdrawals and the collection source for deposits. Ensure it matches an active Mobile Money wallet.

On this page