Bonaventure OgetoBy Bonaventure Ogeto|

Paystack Invalid Amount Error and Kobo Confusion

Paystack expects the amount field to be a positive integer representing the smallest currency unit (kobo for NGN, pesewas for GHS, cents for ZAR/KES/USD). Sending a decimal like 500.50, a string like "5000", or a Naira value instead of kobo will return an invalid amount error. Convert correctly: Math.round(nairaAmount * 100) for NGN, and always send a number, not a string.

What the Error Looks Like

Paystack returns different error messages depending on what is wrong with the amount, but they all come back as a 400 Bad Request:

// Sending a decimal
{
  "status": false,
  "message": "Invalid amount"
}

// Sending zero or negative
{
  "status": false,
  "message": "Invalid amount"
}

// Sending a value below the minimum
{
  "status": false,
  "message": "Amount cannot be less than 10000"
}

The generic "Invalid amount" message does not tell you exactly what is wrong. That is why understanding the rules upfront matters. The amount must be: (1) a number, not a string, (2) a positive integer, no decimals, (3) in the smallest currency unit, and (4) above the currency's minimum threshold.

The Four Ways Amount Goes Wrong

1. Sending Naira instead of kobo

You want to charge 5000 NGN. You send amount: 5000. Paystack reads that as 5000 kobo, which is 50 NGN. If you intended 5000 NGN, you needed to send amount: 500000.

// WRONG
{ amount: 5000 }     // = 50 NGN, not 5000 NGN

// RIGHT
{ amount: 500000 }   // = 5000 NGN

This is the single most common Paystack mistake. The fix is simple: multiply by 100. But where you do the multiplication matters.

2. Sending a decimal

You want to charge 49.99 NGN. You calculate 49.99 * 100 = 4999, but in JavaScript, 49.99 * 100 is actually 4998.999999999999. If you do not round, you send a non-integer and Paystack rejects it.

// WRONG: float precision bug
const amount = 49.99 * 100;
console.log(amount); // 4998.999999999999
// Paystack will reject this decimal

// RIGHT: round after multiplying
const amount = Math.round(49.99 * 100);
console.log(amount); // 4999

3. Sending a string

Your frontend form sends the amount as a string. Your server passes it straight through to the Paystack request body without converting:

// WRONG: amount is a string from the form
const amount = req.body.amount; // "5000" (string)
body: JSON.stringify({ email, amount }) // { "amount": "5000" }

// RIGHT: parse to number first
const amount = parseInt(req.body.amount, 10);
if (isNaN(amount)) throw new Error('Invalid amount');
body: JSON.stringify({ email, amount }) // { "amount": 5000 }

JSON serialization of a string produces "5000" (with quotes), while serialization of a number produces 5000 (no quotes). Paystack's API may reject the string form.

4. Floating-point chain operations

You calculate a subtotal, apply a discount, add tax, then convert to kobo. Each floating-point operation can introduce tiny errors that compound:

// DANGEROUS: chaining float operations
const subtotal = 199.99;
const discount = subtotal * 0.1;    // 19.999...
const afterDiscount = subtotal - discount; // 179.991...
const tax = afterDiscount * 0.075;  // 13.4993...
const total = afterDiscount + tax;  // 193.4903...
const kobo = total * 100;           // 19349.03... (not an integer)

// SAFER: round to kobo at the very end
const subtotal = 199.99;
const discount = subtotal * 0.1;
const afterDiscount = subtotal - discount;
const tax = afterDiscount * 0.075;
const total = afterDiscount + tax;
const kobo = Math.round(total * 100); // 19349

The Safe Conversion Function

Build one function that handles all the conversion logic. Use it everywhere you convert a major-unit amount to Paystack's minor-unit format.

/**
 * Convert an amount in major currency units to Paystack's minor unit format.
 * Handles floating-point precision, type checking, and minimum validation.
 */
function toPaystackAmount(
  amountInMajorUnit: number | string,
  currency: string = 'NGN'
): number {
  // Step 1: Convert string to number if needed
  const numericAmount = typeof amountInMajorUnit === 'string'
    ? parseFloat(amountInMajorUnit)
    : amountInMajorUnit;

  // Step 2: Validate it is a real number
  if (isNaN(numericAmount) || !isFinite(numericAmount)) {
    throw new Error(`Invalid amount: "${amountInMajorUnit}" is not a valid number.`);
  }

  // Step 3: Check for positive value
  if (numericAmount <= 0) {
    throw new Error(`Amount must be positive. Received: ${numericAmount}`);
  }

  // Step 4: Convert to minor unit and round to avoid float issues
  const minorUnitAmount = Math.round(numericAmount * 100);

  // Step 5: Validate minimum
  const minimums: Record = {
    NGN: 10000,
    GHS: 100,
    ZAR: 100,
    KES: 100,
    USD: 100,
  };

  const minimum = minimums[currency];
  if (minimum && minorUnitAmount < minimum) {
    throw new Error(
      `Amount ${numericAmount} ${currency} is below the minimum of ${minimum / 100} ${currency}.`
    );
  }

  return minorUnitAmount;
}

// Usage
const kobo = toPaystackAmount(5000, 'NGN');    // 500000
const pesewas = toPaystackAmount(50, 'GHS');    // 5000
const cents = toPaystackAmount(19.99, 'USD');   // 1999
const kobo2 = toPaystackAmount('250', 'NGN');   // 25000

This function handles every failure mode: string input, float precision, negative values, below-minimum amounts. Call it once at the point where you build the Paystack request. Do not scatter conversion logic across your codebase.

Converting Back from Kobo for Display

When you receive a webhook or verify a transaction, Paystack returns amounts in the minor unit. You need to convert back to the major unit for display to the user.

/**
 * Convert a Paystack minor-unit amount back to the major currency unit for display.
 */
function fromPaystackAmount(amountInMinorUnit: number): number {
  return amountInMinorUnit / 100;
}

/**
 * Format a Paystack amount for display to the user.
 */
function formatPaystackAmount(amountInMinorUnit: number, currency: string): string {
  const majorAmount = amountInMinorUnit / 100;

  const symbols: Record = {
    NGN: '₦',
    GHS: 'GH₵',
    ZAR: 'R',
    KES: 'KSh',
    USD: '$',
  };

  const symbol = symbols[currency] || currency;
  return `${symbol}${majorAmount.toLocaleString('en', {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  })}`;
}

// Usage in a webhook handler
const amount = event.data.amount;     // 500000 (kobo)
const currency = event.data.currency; // "NGN"
const display = formatPaystackAmount(amount, currency); // "₦5,000.00"

Division by 100 does not have the same precision issues as multiplication because the inputs are always integers. 500000 / 100 is exactly 5000, no rounding needed.

Python and PHP Equivalents

The same principles apply in every language. Here are equivalent safe conversion functions.

Python

from decimal import Decimal, ROUND_HALF_UP

def to_paystack_amount(amount_major: float | str, currency: str = "NGN") -> int:
    """Convert major-unit amount to Paystack minor-unit integer."""
    d = Decimal(str(amount_major))

    if d <= 0:
        raise ValueError(f"Amount must be positive. Received: {amount_major}")

    minor_unit = int((d * 100).to_integral_value(rounding=ROUND_HALF_UP))

    minimums = {"NGN": 10000, "GHS": 100, "ZAR": 100, "KES": 100, "USD": 100}
    minimum = minimums.get(currency, 0)

    if minor_unit < minimum:
        raise ValueError(
            f"Amount {amount_major} {currency} is below the minimum of {minimum / 100} {currency}."
        )

    return minor_unit

# Usage
kobo = to_paystack_amount(5000, "NGN")     # 500000
cents = to_paystack_amount("19.99", "USD") # 1999

Python's Decimal type avoids float precision issues entirely. Pass the amount as a string to Decimal() to preserve exact values.

PHP

function toPaystackAmount(float|string $amount, string $currency = 'NGN'): int
{
    $amount = (float) $amount;

    if ($amount <= 0) {
        throw new InvalidArgumentException("Amount must be positive. Received: {$amount}");
    }

    $minorUnit = (int) round($amount * 100);

    $minimums = [
        'NGN' => 10000,
        'GHS' => 100,
        'ZAR' => 100,
        'KES' => 100,
        'USD' => 100,
    ];

    $minimum = $minimums[$currency] ?? 0;

    if ($minorUnit < $minimum) {
        $majorMin = $minimum / 100;
        throw new InvalidArgumentException(
            "Amount {$amount} {$currency} is below the minimum of {$majorMin} {$currency}."
        );
    }

    return $minorUnit;
}

// Usage
$kobo = toPaystackAmount(5000, 'NGN');     // 500000
$cents = toPaystackAmount('19.99', 'USD'); // 1999

PHP's round() function handles float precision correctly for this use case. Cast the result to int after rounding.

Float Precision Traps to Watch For

These specific values are known to cause problems with naive multiplication in JavaScript. Use them as test cases:

// Values that break without Math.round()
console.log(19.99 * 100);   // 1998.9999999999998 (not 1999)
console.log(0.1 + 0.2);     // 0.30000000000000004 (not 0.3)
console.log(33.33 * 100);   // 3332.9999999999995 (not 3333)
console.log(1.005 * 100);   // 100.49999999999999 (not 100.5)
console.log(9.99 * 100);    // 999.0000000000001 (not 999, but rounds correctly)

// With Math.round(), all of these become correct integers
console.log(Math.round(19.99 * 100));  // 1999
console.log(Math.round(33.33 * 100));  // 3333
console.log(Math.round(1.005 * 100));  // 101

If you are working with prices from a database that stores them as DECIMAL(10,2) or NUMERIC, the database preserves exact decimal values. The problem only appears when those values enter JavaScript's floating-point world. Parse them carefully and round immediately after the multiplication.

For financial applications that do heavy arithmetic (tax calculations, split payments, pro-rated billing), consider using a library like big.js or Dinero.js that handles decimal arithmetic without float precision issues. For a simple Paystack integration where you just multiply by 100, Math.round() is enough.

Testing Your Amount Conversion

describe('toPaystackAmount', () => {
  test('converts Naira to kobo', () => {
    expect(toPaystackAmount(5000, 'NGN')).toBe(500000);
  });

  test('handles float precision for 19.99', () => {
    expect(toPaystackAmount(19.99, 'NGN')).toBe(1999);
  });

  test('handles float precision for 33.33', () => {
    expect(toPaystackAmount(33.33, 'NGN')).toBe(3333);
  });

  test('converts string input', () => {
    expect(toPaystackAmount('250', 'NGN')).toBe(25000);
  });

  test('converts string decimal input', () => {
    expect(toPaystackAmount('19.99', 'USD')).toBe(1999);
  });

  test('rejects negative amounts', () => {
    expect(() => toPaystackAmount(-100, 'NGN')).toThrow('positive');
  });

  test('rejects zero', () => {
    expect(() => toPaystackAmount(0, 'NGN')).toThrow('positive');
  });

  test('rejects NaN', () => {
    expect(() => toPaystackAmount('abc', 'NGN')).toThrow('not a valid number');
  });

  test('rejects amounts below NGN minimum', () => {
    expect(() => toPaystackAmount(50, 'NGN')).toThrow('below the minimum');
  });

  test('accepts amount at NGN minimum', () => {
    expect(toPaystackAmount(100, 'NGN')).toBe(10000);
  });

  test('handles GHS correctly', () => {
    expect(toPaystackAmount(50, 'GHS')).toBe(5000);
  });
});

Run these tests in your CI pipeline. If you ever change the conversion logic, the float precision tests will catch regressions immediately.

Verification Checklist

After implementing the safe conversion function:

  1. Initialize a transaction with a whole number amount. Send 5000 NGN (500000 kobo). Confirm success.
  2. Initialize with a decimal amount. Send 19.99 NGN. Confirm your function converts it to 1999 kobo and the API accepts it.
  3. Check the Paystack dashboard. Open the transaction you just created. Confirm the amount shows as 19.99 NGN, not 19.98 or 20.00.
  4. Test with a string input. Pass "500" as the amount from a form. Confirm it converts correctly to 50000 kobo.
  5. Test with garbage input. Pass "abc" or an empty string. Confirm your function throws a clear error instead of sending NaN to Paystack.
  6. Verify webhook amounts. After a test payment completes, check that the amount in the webhook payload matches what you sent. Convert it back and display it. Confirm the display is correct.

For the related error where the amount is valid but below the minimum, see Paystack Amount Too Low Error. For the complete error reference, see Paystack Errors and Troubleshooting: The Complete Reference.

If you are building your first payment integration and want structured guidance, the McTaba Full-Stack Software and AI Engineering self-paced course covers Paystack integration with code reviews and mentor support.

Key Takeaways

  • Paystack requires the amount to be a positive integer in the smallest currency unit. For NGN, that is kobo (1 NGN = 100 kobo). For GHS, pesewas. For ZAR, KES, and USD, cents. Sending 5000 means 5000 kobo (50 NGN), not 5000 NGN.
  • Sending a decimal like 500.50 will cause an error. Paystack does not accept fractional kobo. Use Math.round() to convert to an integer after multiplying by 100. Never use Math.floor() or Math.ceil() for currency, as they introduce rounding bias.
  • Sending a string like "5000" instead of the number 5000 can cause issues depending on how your JSON serializer handles it. Always ensure the amount is a JavaScript number type, not a string, before serializing to JSON.
  • Floating-point arithmetic is the hidden trap. 19.99 * 100 in JavaScript equals 1998.9999999999998, not 1999. Using Math.round() after multiplication fixes this. Never chain multiple float operations before rounding.
  • Build one conversion function and use it everywhere. Converting in multiple places across your codebase leads to inconsistencies. Centralize the conversion so every Paystack API call goes through the same safe path.
  • Test with amounts that trigger float precision bugs: 19.99, 0.1 + 0.2, 33.33. These are the values that break naive multiplication.

Frequently Asked Questions

Does Paystack accept decimal amounts like 49.99?
No. Paystack requires amounts to be integers in the smallest currency unit. For 49.99 NGN, send 4999 (kobo). If you send 49.99 directly, you will get an "Invalid amount" error. Use <code>Math.round(49.99 * 100)</code> to convert safely.
Why does 19.99 * 100 not equal 1999 in JavaScript?
JavaScript uses IEEE 754 floating-point numbers, which cannot represent 19.99 exactly in binary. The result of <code>19.99 * 100</code> is <code>1998.9999999999998</code>. Wrapping with <code>Math.round()</code> produces the correct integer 1999. This is not a bug in JavaScript; it is how floating-point math works in every language.
Should I store prices in kobo in my database?
It depends on your architecture. Storing in kobo (integers) avoids float precision issues in your database and simplifies the Paystack integration because no conversion is needed. However, it makes display logic harder and confuses team members who think in Naira. Many teams store in the major unit as DECIMAL(10,2) and convert to kobo only when calling Paystack. Either approach works if you are consistent.
What happens if I send a negative amount to Paystack?
Paystack returns a 400 Bad Request with an "Invalid amount" message. Negative amounts are never valid for transaction initialization. If you need to reverse a payment, use the refund endpoint instead of initializing a negative transaction.
Can I send the amount as a string in the JSON body?
You should send it as a JSON number, not a string. While Paystack may sometimes accept string amounts, the documented behavior expects a number. Sending <code>"amount": 5000</code> (number) is correct. Sending <code>"amount": "5000"</code> (string) may work today but is not guaranteed. Always parse your input to a number before JSON serialization.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon