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:

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 against your contact list
      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 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 {AVIANISE_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(config('avianise.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

To use phoneNumber lookup, store your customers in a Contact List with their phone numbers:

await fetch("https://api.quolle.com/v1/contacts/your-list-id", {
  method: "POST",
  headers: { "Authorization": "Bearer qle_your_api_key", "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "customer@example.com",
    firstName: "Amaka",
    phone: "+2348012345678",   // used for USSD lookups
  }),
});

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.

Timing matters. USSD gateways require a response within 5 seconds or the session times out. Always respond to the gateway immediately, then trigger the email in a background thread or job queue — never await the email send before responding.