Seev PlusDocs
Docs
Stellar Anchor

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

RoleWhoIn this API
Sending AnchorYour business / partnerThe API client. Authenticates via SEP-10, registers customers, creates transactions, submits the USDC payment.
Receiving AnchorSeevCash (anchor.seevcash.com)Receives USDC on Stellar, pays out GHS via Mobile Money.
Sending ClientThe partner's end userThe person funding the payment (off-platform).
Receiving ClientThe recipient in GhanaReceives 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 Money

Endpoints

MethodPathAuthDescription
GET/sep31/info—Supported receive assets, fees, limits, funding methods, required fields
POST/sep31/transactionsJWTInitiate a receive transaction
GET/sep31/transactions/:idJWTPoll transaction status
PATCH/sep31/transactions/:idJWTUpdate per-transaction fields (recovery)
PUT/sep31/transactions/:id/callbackJWTRegister 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.toml
NETWORK_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 signatures

Step 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 }
        }
      }
    }
  }
}
FieldMeaning
quotes_supportedtrue — the anchor can deliver GHS in exchange for USDC via a SEP-38 quote.
quotes_requiredfalse — a quote is optional. Without one, GHS is delivered at the live rate at payout time.
funding_methodsHow the anchor delivers the off-chain asset. GHS is paid out via mobile_money.
fee_fixed / fee_percentFee in USDC = fee_fixed + amount * fee_percent / 100.
min_amount / max_amountAccepted USDC amount range.
fields.transactionPer-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

FieldRequiredDescription
amountyesUSDC amount the Sending Anchor will send. Must be within min_amount/max_amount.
asset_codeyesUSDC. Must match an entry in /info.
asset_issuernoUSDC issuer. A missing/invalid value is normalized to the correct Circle issuer for the active network.
sender_idyesSEP-12 id of the sending customer (must be ACCEPTED).
receiver_idyesSEP-12 id of the receiving customer (must be ACCEPTED).
quote_idnoA firm SEP-38 quote id. If present, amounts are validated against and aligned to the quote.
destination_assetnoFor the indicative flow. Must be iso4217:GHS.
funding_methodnoAdvisory. Should be mobile_money; other values are accepted with a warning (delivery is always Mobile Money).
fields.transactionrecommendedMobile 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:

ConditionResponse
Quote not found / expired400 { "error": "quote_id ... not found or expired" }
quote.sell_asset ≠ transaction USDC asset400 { "error": "quote sell_asset ... does not match transaction asset" }
quote.sell_amount ≠ amount400 { "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_id from the 201 response
  • Asset: USDC (the issuer from /info / your asset_issuer)
  • Amount: the amount from the request
  • Memo: text memo equal to stellar_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"
  }
}
FieldMeaning
amount_in / amount_in_assetUSDC received on Stellar, in SEP-38 asset format.
amount_out / amount_out_assetGHS delivered to the receiver (iso4217:GHS).
amount_fee / amount_fee_assetFee (deprecated in the spec in favor of fee_details, still returned).
fee_detailsStructured fee breakdown (total, asset, details[]).
stellar_transaction_idHash of the on-chain USDC payment the anchor matched.
external_transaction_idMobile 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 Content
POST /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

StatusMeaning
pending_senderAwaiting the USDC payment on Stellar.
pending_receiverUSDC received and matched; payout being prepared.
pending_externalGHS submitted to the Mobile Money network; awaiting confirmation.
pending_transaction_info_updateRails data invalid/missing — resend via PATCH (see Recovery).
completedGHS delivered to the receiver via Mobile Money.
expiredA quoted transaction whose quote lapsed before the USDC payment arrived.
errorUnrecoverable 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 failure

Error responses

StatusBodyWhen
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_.

On this page