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
| Method | Path | Description |
|---|---|---|
| GET | /sep12/customer | Get current KYC status and required fields |
| PUT | /sep12/customer | Submit or update KYC data |
| DELETE | /sep12/customer | Delete 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)│ └──────────┘
└──────────────┘| Status | Meaning |
|---|---|
NEEDS_DATA | No KYC submitted yet. Submit required fields. |
PROCESSING | KYC submitted, under review. |
ACCEPTED | KYC approved. Customer can transact. |
NEEDS_INFO | Additional information required. Check fields for what's needed. |
REJECTED | KYC rejected. Customer cannot transact. |
Required fields for Ghana corridor
Individual customers (deposit/withdrawal)
| Field (SEP-9) | Type | Description | Required |
|---|---|---|---|
first_name | string | Customer's first name | Yes |
last_name | string | Customer's last name | Yes |
email_address | string | Email address | Yes |
mobile_number | string | Phone number (e.g., +233241234567) | Yes |
mobile_money_number | string | Mobile money wallet number (E.164 format) | Yes (for mobile funding) |
mobile_money_provider | string | MoMo provider: MTN, VODAFONE, TELECEL, AIRTELTIGO | Yes (for mobile funding) |
id_type | string | Type of ID: ghana_card, passport, voter_id | Yes |
id_number | string | ID document number | Yes |
date_of_birth | string | Date of birth (YYYY-MM-DD) | Yes |
address | string | Residential address | No |
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) | Type | Description | Example |
|---|---|---|---|
mobile_money_number | string | Mobile money wallet number (E.164 format) | +233244123456 |
mobile_money_provider | string | Mobile money network provider | MTN |
Supported providers
| Provider | Value | Phone prefixes |
|---|---|---|
| MTN Mobile Money | MTN | 024, 025, 053, 054, 055, 059 |
| Vodafone Cash | VODAFONE | 020, 050 |
| Telecel Cash | TELECEL | 026, 027, 056, 057 |
| AirtelTigo Money | AIRTELTIGO | 026, 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:
- If mobile money details exist in KYC — the anchor triggers collection immediately (auto mode)
- 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
| Network | Example | Format |
|---|---|---|
| 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.