USSD-Triggered Emails
Send transactional emails instantly when users complete a USSD transaction — mobile money receipts, airtime confirmations, and banking alerts.
What is USSD?
USSD (Unstructured Supplementary Service Data) is the session-based protocol behind
short codes like *737#, *901#, and *822# used for
mobile money, airtime top-up, and bank account management across Africa.
Unlike apps or the web, USSD works on any phone — smartphone or feature phone —
with no internet required.
When a user completes a USSD transaction, your backend receives a callback from the USSD gateway. That's the perfect moment to trigger a confirmation email — while the user is still looking at their screen.
The /v1/emails/ussd endpoint
The POST /v1/emails/ussd endpoint is purpose-built for USSD callbacks:
- Returns a simplified
{ queued: true, ref: emailId }response optimised for USSD gateway response handlers - Accepts a
phoneNumberfield to look up the recipient by phone (if you've stored contacts with phone numbers) - Falls back to direct synchronous delivery if the queue is temporarily unavailable — so the email is never dropped, even during connectivity issues
- Accepts a
metadata.ussdobject for session tracking and audit logs
Example: MTN Mobile Money receipt
// Your USSD callback handler (Express)
app.post("/ussd/callback", async (req, res) => {
const { sessionId, phoneNumber, amount, transactionRef } = req.body;
// Respond to the gateway immediately (must be within ~5 seconds)
res.json({ message: "END Your transaction is complete. Receipt sent." });
// Send receipt email in the background
fetch("https://api.quolle.com/v1/emails/ussd", {
method: "POST",
headers: {
"Authorization": "Bearer qle_your_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "receipts@mail.yourbank.com",
phoneNumber, // looked up across all contacts by phone
subject: "Transaction Receipt",
html: `
<h2>Transaction Successful</h2>
<p>Amount: <strong>₦${amount.toLocaleString()}</strong></p>
<p>Ref: ${transactionRef}</p>
<p>Date: ${new Date().toLocaleString("en-NG")}</p>
`,
metadata: {
ussd: {
sessionId,
shortCode: "*737#",
phoneNumber,
transactionRef,
},
},
}),
}).catch(console.error);
});
import os
import requests
import threading
def send_ussd_receipt(session_id, phone_number, amount, transaction_ref):
requests.post(
"https://api.quolle.com/v1/emails/ussd",
headers={"Authorization": f"Bearer {os.environ['QUOLLE_API_KEY']}"},
json={
"from": "receipts@mail.yourbank.com",
"phoneNumber": phone_number,
"subject": "Transaction Receipt",
"html": f"<p>₦{amount:,.0f} sent. Ref: {transaction_ref}</p>",
"metadata": {
"ussd": {
"sessionId": session_id,
"shortCode": "*901#",
"phoneNumber": phone_number,
"transactionRef": transaction_ref,
}
},
},
)
@app.route("/ussd/callback", methods=["POST"])
def ussd_callback():
data = request.json
# Fire-and-forget receipt email
threading.Thread(
target=send_ussd_receipt,
args=(data["sessionId"], data["phoneNumber"],
data["amount"], data["transactionRef"])
).start()
return {"message": "END Transaction complete. Receipt sent."}
<?php
Route::post('/ussd/callback', function (Request $request) {
// Respond to gateway first
$response = response()->json([
'message' => 'END Transaction complete. Receipt sent.'
]);
// Send receipt in background via queue
SendUssdReceipt::dispatch(
$request->sessionId,
$request->phoneNumber,
$request->amount,
$request->transactionRef
);
return $response;
});
// In App\Jobs\SendUssdReceipt:
Http::withToken(env('QUOLLE_API_KEY'))
->post('https://api.quolle.com/v1/emails/ussd', [
'from' => 'receipts@mail.yourbank.com',
'phoneNumber' => $this->phoneNumber,
'subject' => 'Transaction Receipt',
'html' => "<p>₦{$this->amount} sent.</p>",
'metadata' => [
'ussd' => [
'sessionId' => $this->sessionId,
'shortCode' => '*737#',
'phoneNumber' => $this->phoneNumber,
'transactionRef' => $this->transactionRef,
],
],
]);
Using a recipient email address
If you already know the recipient's email, pass to instead of
phoneNumber:
{
"from": "alerts@mail.yourfintech.com",
"to": "customer@example.com",
"subject": "Airtime purchase confirmed",
"html": "<p>₦500 airtime added to +234801234…</p>",
"metadata": {
"ussd": {
"sessionId": "sess_abc123",
"shortCode": "*822#",
"phoneNumber": "+2348012345678"
}
}
}
Linking phone numbers to contacts
When phoneNumber is set, Quolle searches all contacts
in your account for a match. No list ID or extra field is required — the lookup
spans your entire contact list automatically.
Contacts are managed in the dashboard under Contacts. Store your customers there with their phone numbers and the lookup works without any additional configuration on each request.
If no match is found, the endpoint returns:
// HTTP 404
{
"error": "No contact found for this phone number"
}
Response format
The /v1/emails/ussd endpoint returns a minimal response
optimised for quick processing in gateway handlers:
{
"queued": true,
"ref": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
USSD email tracking
Emails sent via the USSD endpoint are tagged with a USSD badge in your Logs dashboard. Filter by source to see all USSD-triggered emails and their delivery status.
await the
email send before responding.