Skip to main content

USSD Payments

USSD payments let any Nigerian phone user pay you — even without a smartphone, app, or internet data.


What is USSD?

USSD (Unstructured Supplementary Service Data) is a protocol built into every GSM mobile network for sending short text exchanges between a phone and the network in real time. Unlike SMS, it's a live interactive session — the phone displays a menu, the user types a response, the menu updates instantly.

Dialling *347# on any Nigerian phone number opens an interactive text menu. No smartphone, no app, no data connection required — it works on the most basic feature phone, anywhere there's GSM signal.

This is how the majority of Nigerians without smartphones conduct mobile banking today. For your product, supporting USSD means reaching 100% of your potential users, not just the subset who own smartphones and have mobile data.


How PayKore USSD works

  1. Your server calls POST /v1/payments/ussd/initiate with the amount and merchant wallet.
  2. PayKore creates a payment session and returns a session ID and a dial code.
  3. You display the dial code to your user — e.g. "Dial *347*000*5000# to pay ₦5,000."
  4. The user dials that exact code on their phone.
  5. The user confirms the payment and enters their 4-digit wallet PIN in the USSD menu.
  6. PayKore processes the debit from the user's wallet and credit to the merchant wallet.
  7. PayKore fires a transaction.completed webhook to your server.
  8. You show the user a success screen (typically via a polling check or webhook-driven update).

Initiating a USSD payment

curl -X POST https://api.paykore.dev/v1/payments/ussd/initiate \
-H "X-API-Key: sk_test_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"payer_wallet_id": "wlt_9f3kA2mXpQ",
"merchant_wallet_id": "wlt_mErChAnT456",
"amount_kobo": 500000,
"merchant_name": "Chukwu Stores",
"reference": "order_789"
}'

Response:

{
"session_id": "ussd_7gHjK2lMnO",
"dial_code": "*347*000*5000#",
"amount_kobo": 500000,
"amount_naira": "5000.00",
"status": "initiated",
"expires_at": "2025-06-01T12:03:00Z",
"meta": {
"request_id": "req_9pQrS4tUvW"
}
}
warning

Sessions expire 3 minutes after creation (expires_at). This window is fixed by telco infrastructure and cannot be extended. Display the dial code to the user immediately, with a visible countdown.


USSD session states

StatusMeaning
initiatedSession created. User hasn't dialled yet.
menu_mainUser has dialled and is viewing the confirmation menu.
pin_entryUser confirmed and is entering their 4-digit PIN.
processingPIN submitted and verified; wallet debit/credit in flight.
completedPayment successful.
failedUser cancelled, or entered the wrong PIN 3 times.
expiredThe 3-minute TTL elapsed before completion.

Displaying the payment prompt to users

Show the dial code prominently, with a live countdown tied to expires_at, and a way for the user to check status without waiting passively for a webhook-driven page refresh.

// React countdown component
function USSDPrompt({ dialCode, expiresAt, sessionId }: USSDPromptProps) {
const [secondsLeft, setSecondsLeft] = useState(
Math.max(0, Math.floor((new Date(expiresAt).getTime() - Date.now()) / 1000))
);

useEffect(() => {
const interval = setInterval(() => {
setSecondsLeft((s) => Math.max(0, s - 1));
}, 1000);
return () => clearInterval(interval);
}, []);

return (
<div>
<p>Dial this code on your phone:</p>
<h2>{dialCode}</h2>
<p>{secondsLeft > 0 ? `Expires in ${secondsLeft}s` : 'Session expired'}</p>
<button onClick={() => checkStatus(sessionId)}>Check payment status</button>
</div>
);
}
tip

Pair the countdown with a "Check payment status" button that calls GET /v1/payments/ussd/{sessionId}. Some users complete the USSD flow before your webhook arrives — polling on demand gives them instant feedback without waiting for the async webhook.


Handling completion

Listen for the transaction.completed webhook with type: "ussd_payment":

{
"event": "transaction.completed",
"data": {
"id": "tx_3kLmN4oPqR",
"type": "ussd_payment",
"status": "completed",
"reference": "order_789",
"amount_kobo": 500000,
"session_id": "ussd_7gHjK2lMnO",
"completed_at": "2025-06-01T12:01:45Z"
},
"timestamp": "2025-06-01T12:01:46Z",
"webhook_id": "wh_6vWxY7zAbC"
}

On receipt: look up your order by reference, mark it as paid, and notify the user (push notification, in-app update, or SMS — USSD users often won't have your app open).


USSD limitations to know

  • 3-minute session timeout — fixed by telco infrastructure, not configurable.
  • Sufficient wallet balance required upfront — the user's PayKore wallet must already hold enough funds before dialling. There's no top-up step mid-flow.
  • No partial payments — a USSD session is all-or-nothing for the specified amount.
  • 3 PIN attempts maximum — after 3 incorrect PIN entries, the session fails and a new one must be initiated.

Sandbox testing

You cannot dial a real shortcode in sandbox — there's no live telco connection. Instead, simulate the user's action directly:

# Simulate a successful confirmation
curl -X POST https://api.paykore.dev/v1/sandbox/ussd/simulate \
-H "X-API-Key: sk_test_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"session_id": "ussd_7gHjK2lMnO", "action": "confirm"}'

# Simulate a cancellation
curl -X POST https://api.paykore.dev/v1/sandbox/ussd/simulate \
-H "X-API-Key: sk_test_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"session_id": "ussd_7gHjK2lMnO", "action": "cancel"}'

Both calls fire the corresponding webhook (transaction.completed or transaction.failed) immediately, letting you fully test your handler without a real phone.


Next steps