SEP-31: Cross-Border Payments
End-to-end guide to the SeevCash SEP-31 Direct Payment API — from SEP-10 authentication and SEP-12 KYC through SEP-38 quoting, transaction creation, the on-chain USDC payment, and the off-chain Ghana Cedis (GHS) Mobile Money payout.
SEP-31 defines a business-to-business "Direct Payment" API. A Sending Anchor (a licensed partner business) delivers USDC on Stellar to the SeevCash anchor, and SeevCash — the Receiving Anchor — pays out the equivalent Ghana Cedis (GHS) to a Receiving Client via Mobile Money.
Only USDC is received on-chain. GHS is off-chain fiat — delivered through Mobile Money and never issued as a Stellar asset. The GHS payout amount is either locked with a SEP-38 quote (stellar:USDC → iso4217:GHS) or computed at the live rate at payout time.
Roles
| Role | Who | In this API |
|---|---|---|
| Sending Anchor | Your business / partner | The API client. Authenticates via SEP-10, registers customers, creates transactions, submits the USDC payment. |
| Receiving Anchor | SeevCash (anchor.seevcash.com) | Receives USDC on Stellar, pays out GHS via Mobile Money. |
| Sending Client | The partner's end user | The person funding the payment (off-platform). |
| Receiving Client | The recipient in Ghana | Receives GHS to their Mobile Money wallet. |
End-to-end flow
Sending Anchor SeevCash (Receiving Anchor)
│ │
│ 0. GET /.well-known/stellar.toml │
│────────────────────────────────────────────────────►│ SEP-1 discovery
│ 1. GET /sep31/info │
│────────────────────────────────────────────────────►│ assets, fees, fields
│ 2. SEP-10 auth (GET /auth → sign → POST /auth) │
│◄───────────────────────────────────────────────────►│ → JWT
│ 3. SEP-12 PUT /customer (sender + receiver) │
│────────────────────────────────────────────────────►│ → customer ids, ACCEPTED
│ 4. SEP-38 POST /quote (optional, locks the rate) │
│────────────────────────────────────────────────────►│ → quote_id
│ 5. POST /sep31/transactions │
│────────────────────────────────────────────────────►│ → id + stellar_account_id + memo
│ 6. Submit USDC payment on Stellar (with memo) │
│─────────────────────────► Stellar ─────────────────►│ anchor matches by memo
│ 7. GET /sep31/transactions/:id (or callback) │
│◄───────────────────────────────────────────────────►│ pending_* → completed
│ │ GHS paid out via Mobile MoneyEndpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /sep31/info | — | Supported receive assets, fees, limits, funding methods, required fields |
| POST | /sep31/transactions | JWT | Initiate a receive transaction |
| GET | /sep31/transactions/:id | JWT | Poll transaction status |
| PATCH | /sep31/transactions/:id | JWT | Update per-transaction fields (recovery) |
| PUT | /sep31/transactions/:id/callback | JWT | Register a status-change callback URL |
All endpoints except /sep31/info require a valid SEP-10 JWT in the Authorization: Bearer <token> header. A missing or invalid token returns 403 Forbidden. Sender and receiver must be registered via SEP-12 KYC and reach ACCEPTED before a transaction can be created.
Step 0 — Discover the anchor (SEP-1)
Read the anchor's stellar.toml to get the endpoint URLs and the signing key used to verify callbacks.
curl https://anchor.seevcash.com/.well-known/stellar.tomlNETWORK_PASSPHRASE = "Test SDF Network ; September 2015"
WEB_AUTH_ENDPOINT = "https://anchor.seevcash.com/auth" # SEP-10
KYC_SERVER = "https://anchor.seevcash.com/sep12" # SEP-12
DIRECT_PAYMENT_SERVER = "https://anchor.seevcash.com/sep31" # SEP-31
ANCHOR_QUOTE_SERVER = "https://anchor.seevcash.com/sep38" # SEP-38
SIGNING_KEY = "GA2E...MPVR" # verifies SEP-31 callback signaturesStep 1 — GET /info: supported assets
Advertises what the anchor can receive, the fee model, limits, SEP-38 quote support, delivery (funding) methods, and the SEP-12 customer types.
curl https://anchor.seevcash.com/sep31/info{
"receive": {
"USDC": {
"enabled": true,
"quotes_supported": true,
"quotes_required": false,
"funding_methods": ["mobile_money"],
"fee_fixed": 0.5,
"fee_percent": 1.0,
"min_amount": 10,
"max_amount": 10000,
"sep12": {
"sender": { "types": { "sep31_sender": { "description": "The person initiating the payment" } } },
"receiver": { "types": { "sep31_receiver": { "description": "The person receiving the payment" } } }
},
"fields": {
"transaction": {
"receiver_routing_number": { "description": "Mobile money provider code for the GHS payout", "optional": false },
"receiver_account_number": { "description": "Mobile money account number for the GHS payout", "optional": false },
"type": { "description": "Type of payout (e.g., mobile_money, bank_transfer)", "optional": true }
}
}
}
}
}| Field | Meaning |
|---|---|
quotes_supported | true — the anchor can deliver GHS in exchange for USDC via a SEP-38 quote. |
quotes_required | false — a quote is optional. Without one, GHS is delivered at the live rate at payout time. |
funding_methods | How the anchor delivers the off-chain asset. GHS is paid out via mobile_money. |
fee_fixed / fee_percent | Fee in USDC = fee_fixed + amount * fee_percent / 100. |
min_amount / max_amount | Accepted USDC amount range. |
fields.transaction | Per-transaction rails data the receiver payout needs (Mobile Money number + provider). |
receive lists only USDC because that is the asset received on Stellar. The GHS payout is an off-chain Mobile Money disbursement modeled as iso4217:GHS in SEP-38, keeping SEP-31 and SEP-38 consistent.
Step 2 — Authenticate (SEP-10)
Obtain a JWT for the Sending Anchor's Stellar account. See SEP-10: Authentication for the full flow.
# 1. Request a challenge
curl "https://anchor.seevcash.com/auth?account=G<SENDING_ANCHOR_PUBLIC_KEY>"
# → { "transaction": "<challenge XDR>", "network_passphrase": "..." }
# 2. Sign the challenge, then submit it
curl -X POST "https://anchor.seevcash.com/auth" \
-H "Content-Type: application/json" \
-d '{ "transaction": "<signed challenge XDR>" }'
# → { "token": "<JWT>" }Use the returned JWT as Authorization: Bearer <JWT> on every call below.
Step 3 — Register sender and receiver (SEP-12 KYC)
Both customers must exist and reach ACCEPTED. See SEP-12: KYC. The receiver must supply Mobile Money payout details (number + provider) so the GHS disbursement can be made.
curl -X PUT "https://anchor.seevcash.com/sep12/customer" \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{
"type": "sep31_sender",
"first_name": "Alice",
"last_name": "Sender",
"email_address": "alice@example.com"
}'
# → { "id": "c1a2...sender-id" }curl -X PUT "https://anchor.seevcash.com/sep12/customer" \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{
"type": "sep31_receiver",
"first_name": "Bob",
"last_name": "Receiver",
"email_address": "bob@example.com",
"mobile_number": "+233241234567",
"mobile_money_provider": "MTN"
}'
# → { "id": "c3b4...receiver-id" }curl "https://anchor.seevcash.com/sep12/customer?id=c3b4...receiver-id" \
-H "Authorization: Bearer <JWT>"
# → { "id": "c3b4...receiver-id", "status": "ACCEPTED", "provided_fields": { ... } }POST /sep31/transactions is rejected unless both sender_id and receiver_id resolve to customers with status ACCEPTED. See Customer info needed below.
Step 4 — Get a firm quote (SEP-38, optional)
Lock the USDC → GHS rate before creating the transaction. Skip this to use the live rate at payout time. See Quotes (SEP-38).
curl -X POST "https://anchor.seevcash.com/sep38/quote" \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{
"sell_asset": "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
"buy_asset": "iso4217:GHS",
"sell_amount": "100",
"context": "sep31"
}'{
"id": "quote_1733000000_ab12cd34",
"expires_at": "2026-10-05T11:15:00Z",
"total_price": "13.80",
"price": "13.38",
"sell_amount": "100.00",
"sell_asset": "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
"buy_amount": "1380.00",
"buy_asset": "iso4217:GHS",
"fee": { "total": "3.00", "asset": "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" }
}A quoted transaction expires with the quote. If the USDC payment is not received before expires_at, the transaction moves to expired and must be recreated with a fresh quote.
Step 5 — POST /transactions: initiate the receive
There are three valid request shapes. All return the same response: the Stellar account and memo to pay to. The quote_id and destination_asset fields are optional.
GHS delivered at the live rate at payout time.
curl -X POST "https://anchor.seevcash.com/sep31/transactions" \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{
"amount": "100",
"asset_code": "USDC",
"asset_issuer": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
"sender_id": "c1a2...sender-id",
"receiver_id": "c3b4...receiver-id",
"funding_method": "mobile_money",
"fields": {
"transaction": {
"receiver_account_number": "0241234567",
"receiver_routing_number": "MTN"
}
}
}'Rate locked to the SEP-38 quote. Add quote_id.
curl -X POST "https://anchor.seevcash.com/sep31/transactions" \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{
"amount": "100",
"asset_code": "USDC",
"asset_issuer": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
"quote_id": "quote_1733000000_ab12cd34",
"sender_id": "c1a2...sender-id",
"receiver_id": "c3b4...receiver-id",
"funding_method": "mobile_money",
"fields": {
"transaction": {
"receiver_account_number": "0241234567",
"receiver_routing_number": "MTN"
}
}
}'Estimated rate via destination_asset (no firm quote).
curl -X POST "https://anchor.seevcash.com/sep31/transactions" \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{
"amount": "100",
"asset_code": "USDC",
"asset_issuer": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
"destination_asset": "iso4217:GHS",
"sender_id": "c1a2...sender-id",
"receiver_id": "c3b4...receiver-id",
"funding_method": "mobile_money",
"fields": {
"transaction": {
"receiver_account_number": "0241234567",
"receiver_routing_number": "MTN"
}
}
}'{
"id": "550e8400-e29b-41d4-a716-446655440000",
"stellar_account_id": "GA2E5D24D2LCGVA3SVVKDP64OTN2M2NLUWRDYINB352HW35ABZ66MPVR",
"stellar_memo_type": "text",
"stellar_memo": "550e8400e29b41d4a716446655"
}Request parameters
| Field | Required | Description |
|---|---|---|
amount | yes | USDC amount the Sending Anchor will send. Must be within min_amount/max_amount. |
asset_code | yes | USDC. Must match an entry in /info. |
asset_issuer | no | USDC issuer. A missing/invalid value is normalized to the correct Circle issuer for the active network. |
sender_id | yes | SEP-12 id of the sending customer (must be ACCEPTED). |
receiver_id | yes | SEP-12 id of the receiving customer (must be ACCEPTED). |
quote_id | no | A firm SEP-38 quote id. If present, amounts are validated against and aligned to the quote. |
destination_asset | no | For the indicative flow. Must be iso4217:GHS. |
funding_method | no | Advisory. Should be mobile_money; other values are accepted with a warning (delivery is always Mobile Money). |
fields.transaction | recommended | Mobile Money payout rails. If supplied, required fields advertised in /info must be complete. May also be provided via the receiver's SEP-12 record. |
The stellar_memo is the transaction id (UUID, dashes removed) truncated to 28 characters — a Stellar text memo. The Sending Anchor must attach this exact memo to the USDC payment; the anchor matches incoming payments by memo only.
Quote validation (when quote_id is used)
The anchor rejects the request if the quote does not line up with the transaction:
| Condition | Response |
|---|---|
| Quote not found / expired | 400 { "error": "quote_id ... not found or expired" } |
quote.sell_asset ≠ transaction USDC asset | 400 { "error": "quote sell_asset ... does not match transaction asset" } |
quote.sell_amount ≠ amount | 400 { "error": "quote sell_amount ... does not match requested amount" } |
On success the stored amounts are aligned to the quote: amount_in = sell_amount, amount_out = buy_amount, amount_fee = fee.total, amount_fee_asset = fee.asset.
Step 6 — Submit the USDC payment on Stellar
Off-protocol. The Sending Anchor builds a Stellar payment operation and submits it to Horizon:
- Destination:
stellar_account_idfrom the 201 response - Asset:
USDC(the issuer from/info/ yourasset_issuer) - Amount: the
amountfrom the request - Memo:
textmemo equal tostellar_memo
The anchor's payment monitor detects the payment, matches it by memo, and begins the GHS payout.
Step 7 — GET /transactions/:id: poll status
curl "https://anchor.seevcash.com/sep31/transactions/550e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer <JWT>"amount_out is omitted on the no-quote path until the payment arrives (it is only known once the GHS amount is computed). With a quote_id it is present from creation.
{
"transaction": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending_sender",
"amount_in": "100.00",
"amount_in_asset": "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
"amount_out_asset": "iso4217:GHS",
"amount_fee": "1.50",
"amount_fee_asset": "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
"fee_details": {
"total": "1.50",
"asset": "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
"details": [ { "name": "Anchor fee", "amount": "1.50" } ]
},
"stellar_account_id": "GA2E5D24D2LCGVA3SVVKDP64OTN2M2NLUWRDYINB352HW35ABZ66MPVR",
"stellar_memo": "550e8400e29b41d4a716446655",
"stellar_memo_type": "text",
"started_at": "2026-10-05T11:00:00Z",
"updated_at": "2026-10-05T11:00:00Z",
"sender_id": "c1a2...sender-id",
"receiver_id": "c3b4...receiver-id"
}
}{
"transaction": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"amount_in": "100.00",
"amount_in_asset": "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
"amount_out": "1380.00",
"amount_out_asset": "iso4217:GHS",
"amount_fee": "1.50",
"amount_fee_asset": "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
"fee_details": {
"total": "1.50",
"asset": "stellar:USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
"details": [ { "name": "Anchor fee", "amount": "1.50" } ]
},
"stellar_account_id": "GA2E5D24D2LCGVA3SVVKDP64OTN2M2NLUWRDYINB352HW35ABZ66MPVR",
"stellar_memo": "550e8400e29b41d4a716446655",
"stellar_memo_type": "text",
"stellar_transaction_id": "3389e9f0f1a7...",
"external_transaction_id": "MOMO-REF-123456",
"started_at": "2026-10-05T11:00:00Z",
"updated_at": "2026-10-05T11:02:30Z",
"completed_at": "2026-10-05T11:02:30Z",
"sender_id": "c1a2...sender-id",
"receiver_id": "c3b4...receiver-id"
}
}| Field | Meaning |
|---|---|
amount_in / amount_in_asset | USDC received on Stellar, in SEP-38 asset format. |
amount_out / amount_out_asset | GHS delivered to the receiver (iso4217:GHS). |
amount_fee / amount_fee_asset | Fee (deprecated in the spec in favor of fee_details, still returned). |
fee_details | Structured fee breakdown (total, asset, details[]). |
stellar_transaction_id | Hash of the on-chain USDC payment the anchor matched. |
external_transaction_id | Mobile Money reference for the GHS payout — used to reconcile on-chain receipt against off-chain disbursement. |
A non-existent id returns 404 { "error": "Transaction not found" }.
Step 8 — Status callbacks (optional, instead of polling)
Register a URL and the anchor will POST the full transaction object on every status change — signed so you can verify it came from the anchor.
curl -X PUT "https://anchor.seevcash.com/sep31/transactions/550e8400-e29b-41d4-a716-446655440000/callback" \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{ "url": "https://partner.example.com/sep31/callback" }'
# → 204 No ContentPOST /sep31/callback HTTP/1.1
Host: partner.example.com
Content-Type: application/json
Signature: t=1733000000, s=<base64 ed25519 signature>
X-Stellar-Signature: t=1733000000, s=<same signature>
{ "transaction": { "...": "same shape as GET /transactions/:id" } }Verify the signature. The signature covers the string <timestamp>.<your-host>.<raw-body> and is signed with the anchor's SIGNING_KEY (from stellar.toml). Reject callbacks whose timestamp is older than ~2 minutes to prevent replay. The deprecated X-Stellar-Signature header carries the same value for backward compatibility.
Recovery — PATCH /transactions/:id
If the Mobile Money payout cannot proceed because the per-transaction rails data is missing or invalid, the transaction moves to pending_transaction_info_update with a required_info_updates object describing exactly what to resend.
{
"transaction": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending_transaction_info_update",
"required_info_message": "Missing receiver mobile-money payout details (phone/provider) — cannot deliver GHS",
"required_info_updates": {
"transaction": {
"receiver_account_number": { "description": "Mobile money account number for the GHS payout" }
}
}
}
}curl -X PATCH "https://anchor.seevcash.com/sep31/transactions/550e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{
"fields": {
"transaction": {
"receiver_account_number": "0241234567"
}
}
}'Once the corrected fields validate, the transaction returns to pending_receiver and the payout is retried automatically.
Transaction statuses
| Status | Meaning |
|---|---|
pending_sender | Awaiting the USDC payment on Stellar. |
pending_receiver | USDC received and matched; payout being prepared. |
pending_external | GHS submitted to the Mobile Money network; awaiting confirmation. |
pending_transaction_info_update | Rails data invalid/missing — resend via PATCH (see Recovery). |
completed | GHS delivered to the receiver via Mobile Money. |
expired | A quoted transaction whose quote lapsed before the USDC payment arrived. |
error | Unrecoverable failure; contact the anchor. |
pending_sender → pending_receiver → pending_external → completed
│
└▶ pending_transaction_info_update ──(PATCH)──▶ pending_receiver
expired ← quote lapsed before funding
error ← unrecoverable failureError responses
| Status | Body | When |
|---|---|---|
| 403 | — | Missing or invalid SEP-10 JWT. |
| 400 | { "error": "customer_info_needed", "type": "sep31_sender", "message": "..." } | A customer is missing or not ACCEPTED. Re-run SEP-12 for that type, then retry. |
| 400 | { "error": "transaction_info_needed", "fields": { "transaction": { ... } } } | Required per-transaction fields were incomplete. Resend with the listed fields. |
| 400 | { "error": "Amount must be between 10 and 10000" } | amount outside the configured range. |
| 400 | { "error": "quote_id ... not found or expired" } | Supplied quote_id is invalid/expired. |
| 404 | { "error": "Transaction not found" } | Unknown transaction id on GET/PATCH/callback. |
Reconciliation
Each completed transaction links an on-chain receipt to an off-chain disbursement:
stellar_transaction_id— the Horizon hash of the USDC payment received.external_transaction_id— the Mobile Money provider reference for the GHS payout.
Match these against your own ledger. During testing the Mobile Money leg is simulated (no live payout), and external_transaction_id is prefixed SIMULATED_WTH_.
Related
- SEP-10: Authentication — Obtain the JWT.
- SEP-12: KYC — Register sender and receiver.
- Quotes (SEP-38) — Price / lock the USDC → GHS conversion.
- stellar.toml — Discovery and signing key.
- Stellar SEP-31 Specification