dánwòdánwò

Payments

Paystack

Transactions paid on Paystack's hosted page with its test cards, bank transfer or USSD, plus plans, transfers, refunds and signed events.

danwo's Paystack emulator answers api.paystack.co and the hosted payment page on checkout.paystack.com. Your app initializes a transaction, the browser pays on the hosted page, and the emulator sends charge.success to your webhook URL.

Set up

Add paystackService and seed an integration with the secret key your app uses. Hand the same key to the app:

danwo.config.ts
import { appEnvironment, defineConfig, paystackService } from "@danwo/sdk"

const url = "http://localhost:3000"
const secretKey = "sk_test_danwo"

export default defineConfig({
  testDir: "tests",
  readiness: { mode: "page+backend", settle: true },
  environment: () =>
    appEnvironment({
      app: { command: "npm run dev", url, env: { PAYSTACK_SECRET_KEY: secretKey } },
      services: [
        paystackService({
          seed: {
            integrations: [
              {
                secretKey,
                webhookUrl: `${url}/api/webhooks/paystack`,
                callbackUrl: `${url}/paid`,
              },
            ],
          },
        }),
      ],
    }),
})

A key the emulator doesn't know gets Paystack's own 401, Invalid key.

Seed it

danwo.config.ts · the seed
paystackService({
  seed: {
    integrations: [{ secretKey: "sk_test_danwo", currency: "NGN", balance: { NGN: 5_000_000 } }],
    customers: [{ email: "[email protected]", firstName: "Ada" }],
    plans: [{ planCode: "PLN_pro", name: "Pro", amount: 500_000, interval: "monthly" }],
    accounts: [{ bankCode: "058", accountNumber: "0123456789", accountName: "Ada Obi" }],
  },
})
FieldWhat it seeds
integrationsSecret keys, the webhook and callback URLs, the currency, the balance and whether transfers need an OTP.
customersCustomers, attached to the first integration unless you name one.
plansPlans, hourly to annually, with an optional invoice limit.
accountsBank accounts that resolve by number, and what a transfer to each does.

Amounts are in the currency's subunit, kobo for naira, as on Paystack.

Use it in a test

Pay with a test card on the hosted page, then read the ledger:

tests/pay.danwo.ts
import { type PaystackFixtures, paystackLedger, test as base } from "@danwo/sdk"

const test = base.extend<PaystackFixtures>()

test("pays for an order once", async ({ app, screen, expect, paystack }) => {
  await app.open("/checkout")
  await screen.getByRole("button", { name: "Pay with Paystack" }).click()

  await screen.getByLabel("Card number").fill("4084 0840 8408 4081")
  await screen.getByLabel("Expiry (MM/YY)").fill("12/30")
  await screen.getByLabel("CVV").fill("408")
  await screen.getByRole("button", { name: /^Pay / }).click()
  await expect(screen.getByText("Order paid")).toBeVisible()

  const ledger = paystackLedger(paystack)

  expect(ledger.successful("NGN")).toHaveLength(1)
  expect(ledger.paidTotal("[email protected]")).toBe(500_000)
})
HelperWhat it's for
paystackLedger(paystack)Transactions, transfers, refunds, customers and subscriptions, and totals paid.
paystackBank(paystack)Paying into a dedicated account, as the customer's bank would.

Test cards

Card numberWhat the hosted page does
4084 0840 8408 4081Pays at once.
5060 6666 6666 6666 666Asks for PIN 1234, then pays.
5078 5078 5078 5078 12Asks for PIN 1111, then OTP 123456.
4084 0800 0000 5408Declined.
5399 8383 8383 8381Declined for insufficient funds.

The page also offers bank transfer ("I've sent the money") and USSD, and "Cancel payment" goes back to your callback URL.

What's covered

AreaCovered
TransactionsInitialize, verify, list and total them; charge a saved card again.
Hosted pageCards with PIN and OTP steps, bank transfer, USSD, cancelling.
CustomersCreate, read, update and list them.
Plans and subscriptionsPlans, subscriptions, and renewals on the emulator's clock.
TransfersRecipients and transfers, with an OTP if asked; they settle later.
RefundsFull and partial refunds; they settle later.
BanksBank lists, account resolution, the balance, dedicated accounts.

Faults

CallFaults the matrix tries
A write, such as POST /transaction/initialize429, 500, 503, a 10-second delay, connection reset, answer lost after Paystack acted
GET /transaction/verify/:reference429, 500, connection reset, and a 200 whose body says the payment failed
Every webhook deliveryarrives twice, arrives 10 minutes late, forged signature, 1 hour stale

Webhooks

Events go to the integration's webhookUrl, signed with x-paystack-signature: an HMAC-SHA512 of the body under the secret key. There is no timestamp, as on Paystack, so a stale event is only caught by your own checks.

EventsSent when
charge.successA payment goes through.
transfer.*A transfer succeeds, fails or is reversed.
refund.processedA refund settles.
subscription.*A subscription is created or disabled.
invoice.*A renewal invoice is created, updated or fails.

Fidelity

Live. danwo fidelity paystack runs 4 scenarios against a Paystack test-mode integration. All 4 match.

Not covered yet

  • Split payments and subaccounts
  • Payment pages, invoices you create yourself and the storefront
  • Disputes and settlements reports

Next

On this page

Run it on every pull request

A GitHub check and one comment per pull request, with the flight records attached.

Set up CI