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

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.