McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Terminal Event Webhooks and Receipt Printing
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Terminal Event Webhooks and Receipt Printing

Terminal transactions trigger the same charge.success and charge.failed webhooks as online payments. Extract the reference, amount, channel, and timestamp from the webhook payload. Format a receipt with your business name, transaction details, and a unique receipt number. Send the formatted data to a thermal printer via ESC/POS commands, or generate a digital receipt as PDF or SMS.

In this article

  1. 1. Terminal Webhook Events
  2. 2. Extracting Receipt Data from Webhooks
  3. 3. Formatting a Receipt
  4. 4. Connecting a Thermal Printer
  5. 5. Digital Receipts via SMS and Email
  6. 6. Storing Receipts for Reprints and Disputes

Terminal Webhook Events

Terminal transactions produce the same webhook events as online payments. The key events are:

charge.success: The payment went through. The customer's card was charged successfully. This is your signal to update the order, print a receipt, and release the terminal for the next payment.

charge.failed: The payment did not go through. The card was declined, the PIN was wrong, or the bank rejected the transaction. Show the failure reason to the cashier and let them retry or ask the customer for a different card.

// terminal-webhook.js
var crypto = require('crypto');

function handleTerminalWebhook(req, res) {
  var hash = crypto
    .createHmac('sha512', process.env.PAYSTACK_SECRET_KEY)
    .update(JSON.stringify(req.body))
    .digest('hex');

  if (hash !== req.headers['x-paystack-signature']) {
    return res.status(401).send('Invalid signature');
  }

  var event = req.body;
  var data = event.data;

  if (event.event === 'charge.success') {
    processSuccessfulTerminalPayment(data);
  }

  if (event.event === 'charge.failed') {
    processFailedTerminalPayment(data);
  }

  return res.status(200).send('OK');
}

async function processSuccessfulTerminalPayment(data) {
  var receiptData = {
    reference: data.reference,
    amount: data.amount,
    currency: data.currency,
    channel: data.channel,
    card_type: data.authorization ? data.authorization.brand : 'card',
    last_four: data.authorization ? data.authorization.last4 : '****',
    paid_at: data.paid_at || new Date().toISOString(),
  };

  // Store receipt data
  await storeReceipt(receiptData);

  // Trigger receipt printing
  await printReceipt(receiptData);

  // Update order status
  await markOrderPaid(data.reference);
}

Extracting Receipt Data from Webhooks

The webhook payload contains everything you need for a receipt. Here are the fields that matter.

// extract-receipt-data.js
function extractReceiptFields(webhookData) {
  var data = webhookData.data;

  return {
    transaction_reference: data.reference,
    amount: data.amount,                          // in kobo
    amount_display: formatAmount(data.amount, data.currency),
    currency: data.currency,                      // "NGN", "KES", etc.
    channel: data.channel,                        // "card"
    card_brand: data.authorization ? data.authorization.brand : null,      // "visa", "mastercard"
    card_last4: data.authorization ? data.authorization.last4 : null,      // "4081"
    card_type: data.authorization ? data.authorization.card_type : null,   // "debit"
    bank: data.authorization ? data.authorization.bank : null,
    paid_at: data.paid_at,
    customer_email: data.customer ? data.customer.email : null,
  };
}

function formatAmount(kobo, currency) {
  var amount = kobo / 100;
  if (currency === 'NGN') return 'NGN ' + amount.toLocaleString('en-NG', { minimumFractionDigits: 2 });
  if (currency === 'KES') return 'KES ' + amount.toLocaleString('en-KE', { minimumFractionDigits: 2 });
  return currency + ' ' + amount.toFixed(2);
}

Card last 4 digits. The authorization object in the webhook contains the last 4 digits of the card number. Show this on the receipt so the customer can verify which card was charged. Never show more than the last 4 digits.

Card brand. Show the card network (Visa, Mastercard, Verve) on the receipt. This helps with disputes: "I paid with my Visa ending in 4081."

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

Formatting a Receipt

// format-receipt.js
function formatReceiptText(receiptData, businessInfo) {
  var lines = [];

  // Header
  lines.push(centerText(businessInfo.name, 32));
  lines.push(centerText(businessInfo.address, 32));
  lines.push(centerText(businessInfo.phone, 32));
  lines.push('--------------------------------');

  // Transaction details
  lines.push('Date: ' + formatDate(receiptData.paid_at));
  lines.push('Ref:  ' + receiptData.transaction_reference);
  lines.push('');

  // Amount
  lines.push('AMOUNT: ' + receiptData.amount_display);
  lines.push('');

  // Payment method
  var cardInfo = (receiptData.card_brand || 'Card').toUpperCase();
  cardInfo = cardInfo + ' ****' + (receiptData.card_last4 || '****');
  lines.push('Paid by: ' + cardInfo);
  lines.push('');

  // Footer
  lines.push('--------------------------------');
  lines.push(centerText('Thank you!', 32));
  lines.push(centerText('Powered by Paystack', 32));

  return lines.join('\n');
}

function centerText(text, width) {
  if (text.length >= width) return text;
  var padding = Math.floor((width - text.length) / 2);
  var spaces = '';
  for (var i = 0; i < padding; i++) spaces += ' ';
  return spaces + text;
}

function formatDate(dateString) {
  var d = new Date(dateString);
  return d.toLocaleDateString('en-GB') + ' ' + d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
}

32-character width is standard for most thermal receipt printers. Adjust based on your printer model. Some use 40 or 48 characters per line.

Connecting a Thermal Printer

Thermal printers connect via USB, Bluetooth, or network. They use the ESC/POS command set. Node.js libraries like escpos handle the low-level commands.

// print-receipt.js
// Using escpos library
var escpos = require('escpos');

async function printToThermalPrinter(receiptText) {
  var device = new escpos.USB(); // or escpos.Network('192.168.1.100')
  var printer = new escpos.Printer(device);

  return new Promise(function(resolve, reject) {
    device.open(function(err) {
      if (err) {
        console.error('Printer error: ' + err.message);
        return reject(err);
      }

      printer
        .font('a')
        .align('ct')
        .style('b')
        .size(1, 1)
        .text(receiptText)
        .cut()
        .close();

      resolve();
    });
  });
}

Fallback when the printer is offline. If the printer is not connected or out of paper, do not block the payment flow. Store the receipt in your database and let the cashier reprint later. Show a message: "Receipt saved. Printer not available."

USB vs Network printers. USB printers connect directly to the computer running your POS app. Network printers connect via Wi-Fi or Ethernet and can be shared across multiple registers. Network printers are more flexible but need proper network configuration.

Digital Receipts via SMS and Email

Many customers prefer digital receipts. Offer to send the receipt via SMS or email instead of printing.

// digital-receipt.js
async function sendDigitalReceipt(receiptData, method, destination) {
  var receiptText = formatReceiptText(receiptData, businessInfo);

  if (method === 'sms') {
    await smsService.send(destination, receiptText);
  }

  if (method === 'email') {
    await emailService.send({
      to: destination,
      subject: 'Payment Receipt - ' + receiptData.transaction_reference,
      text: receiptText,
    });
  }

  // Log the delivery
  await db.query(
    'INSERT INTO receipt_deliveries (reference, method, destination, sent_at) VALUES ($1, $2, $3, NOW())',
    [receiptData.transaction_reference, method, destination]
  );
}

Ask before the payment, not after. Collect the customer's phone number or email before pushing the payment to the terminal. After the payment completes, send the receipt automatically. Do not make the customer wait while you ask for their contact details after they have already paid.

Privacy. Only use the phone number or email for the receipt. Do not add them to a marketing list without separate consent.

Storing Receipts for Reprints and Disputes

// store-receipt.js
async function storeReceipt(receiptData) {
  await db.query(
    'INSERT INTO receipts ' +
    '(reference, amount, currency, card_brand, card_last4, paid_at, receipt_text, created_at) ' +
    'VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())',
    [
      receiptData.transaction_reference,
      receiptData.amount,
      receiptData.currency,
      receiptData.card_brand,
      receiptData.card_last4,
      receiptData.paid_at,
      formatReceiptText(receiptData, businessInfo),
    ]
  );
}

async function reprintReceipt(reference) {
  var receipt = await db.query(
    'SELECT receipt_text FROM receipts WHERE reference = $1',
    [reference]
  );

  if (receipt.rows.length === 0) {
    return { error: true, message: 'Receipt not found' };
  }

  await printToThermalPrinter(receipt.rows[0].receipt_text);
  return { error: false };
}

Keep receipts for at least 1 year. Chargebacks and disputes can arise months after the transaction. Having the receipt data proves the transaction details and helps resolve disputes.

Key Takeaways

  • ✓Terminal transactions trigger standard charge.success and charge.failed webhooks. Your existing webhook handler works for terminal payments.
  • ✓The webhook payload includes channel ("card"), card details (last 4 digits, brand), and amount. Use these for receipt data.
  • ✓Format receipts with your business name, date, amount, payment method, reference number, and a unique receipt ID.
  • ✓Thermal printers use ESC/POS commands. Libraries like escpos (Node.js) handle the formatting.
  • ✓Offer digital receipts via SMS or email as an alternative to printed receipts. Collect the customer phone number or email at checkout.
  • ✓Store receipt data in your database for reprints and for dispute resolution.

Frequently Asked Questions

Do Paystack terminals have built-in receipt printers?
Some Paystack terminal models include built-in thermal printers. Others do not and require an external printer connected to your POS system. Check the specifications of your terminal model. If the terminal has a built-in printer, you can send print commands through the Terminal API.
What information must a receipt include by law?
Requirements vary by country and business type. Generally, a receipt should include the business name, date, amount, payment method, and a unique reference number. For VAT-registered businesses, include the VAT number and tax breakdown. Check your local regulations.
Can I customize the receipt format per business or location?
Yes. Store receipt templates in your database, one per business or location. Each template can have a different business name, address, logo, and footer message. Apply the correct template when formatting the receipt.
How do I handle receipt printing failures?
Never let a printing failure block the payment. The payment has already been processed. Store the receipt in your database. Show the cashier a "Receipt saved - printer unavailable" message. Provide a reprint button they can use when the printer is back online.

Learn Paystack Integration

KES 2,999

The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.

Start Paystack Course

Not ready? Create Free Account

Keep reading

Paystack Terminal and In-Person Payments: Complete Guide

Paystack Terminal lets you accept card payments at a physical location using Paystack POS devices. You push a payment request from your server to the terminal, the customer taps or inserts their card, and you get a webhook when it succeeds. This guide covers the full integration.

Read more

Terminal Invoice Payments via API

When a customer walks in with an invoice to pay, your system needs to push the exact invoice amount to the terminal, track which invoice the payment covers, and mark it as settled once the card payment completes. This guide covers the full flow: from invoice lookup to terminal push to webhook reconciliation.

Read more

Building a Terminal App with React Native

If your team already knows React, React Native is the fastest path to a native POS app. This guide shows you how to structure a React Native terminal app, communicate with your backend, manage payment state, and design a tablet-first interface that cashiers can operate under real-world conditions.

Read more

Restaurant Ordering and Terminal Payment Flow

Restaurant payment flows are different from retail. Orders change after they are placed (added dessert, extra drinks). Bills split between customers. Tips are added after the meal. This guide shows you how to build a restaurant ordering system that works with Paystack Terminal for the final payment step.

Read more

Paystack Disputes and Chargebacks: The Developer Playbook

Chargebacks cost you money, time, and sometimes your merchant standing. Paystack gives developers a Dispute API to receive, respond to, and resolve chargebacks programmatically. This guide walks through the dispute lifecycle, evidence submission, webhook events, and code for an automated response system.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.