Skip to content
SendByte

20 Jun 2026 · SendByte Team

Sending receipts and OTPs with Paystack and Flutterwave webhooks

Two of the most important emails a Nigerian app sends are the payment receipt and the one-time passcode. Both are triggered by an event, both have to arrive reliably, and the OTP has to arrive within seconds or it is useless. Here is a clean pattern for both, built around Paystack and Flutterwave webhooks and a transactional email API.

The shape of the flow

For a receipt, the flow is event-driven. The payment provider calls your webhook when a charge succeeds, you verify that the call is genuine, you record the payment, and then you send the receipt. The email send is the last step, after you trust the event.

Paystack / Flutterwave  ->  your webhook  ->  verify signature
                        ->  record payment ->  send receipt email

The verification step is not optional. Anyone can POST to your webhook URL, so you must confirm the request really came from your payment provider before acting on it. Paystack signs the payload with your secret key in an x-paystack-signature header; Flutterwave sends a verif-hash you compare against your configured secret. Reject anything that does not match.

import crypto from 'node:crypto';

function isValidPaystack(rawBody, signature, secretKey) {
  const hash = crypto
    .createHmac('sha512', secretKey)
    .update(rawBody)
    .digest('hex');
  return hash === signature;
}

Sending the receipt

Once the event is verified and the payment is recorded, send the receipt. Use a template so the layout lives in one place, and make the send idempotent: if the provider retries the webhook, which both do, you should not email the customer twice. Key the send on the transaction reference and skip if you have already sent for it.

if (isValidPaystack(rawBody, signature, env.PAYSTACK_SECRET)) {
  const { reference, amount, customer } = event.data;
  if (await alreadyEmailed(reference)) return ok();

  await fetch('https://api.sendbyte.africa/v1/emails', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${env.SENDBYTE_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      from: 'receipts@yourcompany.com',
      to: customer.email,
      template: 'payment-receipt',
      variables: { reference, amount: amount / 100 },
    }),
  });

  await markEmailed(reference);
}

Check the docs for the exact endpoint, the SDKs, and the template payload.

Getting OTP deliverability right

A receipt that arrives a minute late is fine. An OTP that arrives a minute late is a failed login. OTP email has stricter requirements, and most of them are about deliverability rather than code.

Send OTPs from a transactional stream that is not shared with marketing, so a campaign complaint never slows your codes down. Keep the email tiny and plain text where possible, since heavy HTML is slower to process and more likely to be filtered. Put the code in the subject line as well as the body so the user can read it from the notification without opening the mail. Set a short expiry, state it in the message, and rate-limit requests so a user cannot trigger ten identical OTP emails that look like abuse to inbox providers.

await fetch('https://api.sendbyte.africa/v1/emails', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${env.SENDBYTE_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    from: 'security@yourcompany.com',
    to: user.email,
    subject: `${code} is your verification code`,
    text: `Your code is ${code}. It expires in 10 minutes. If this was not you, ignore this email.`,
  }),
});

The reason a transactional-first provider matters here is speed and isolation. Codes go out on infrastructure that is not bogged down by bulk sends, and the reputation that carries them is protected. If your OTPs are arriving late or not at all, the cause is almost always shared-IP reputation or missing authentication, both covered in why your transactional email lands in spam.

Tie it together

Verify the webhook, record the payment, send idempotently, and treat OTP as its own fast, clean stream. Authenticate your domain first with the SPF, DKIM, and DMARC guide, and if you are setting up sending from scratch, start with the 2026 guide to transactional email in Nigeria.