Skip to main content

Accepting Payments

Learn the reviewed public payment flow in the current repo: create charges, verify outcomes, refund when needed, and reuse saved payment methods for recurring billing.
Some older payment docs still reference a hosted-checkout initialize flow. The live reviewed public contract in this repo is the charges, refunds, and tokens surface documented here.

What You’ll Learn

  • create customer-present charges
  • verify charge status
  • process refunds
  • tokenize payment methods safely
  • run recurring merchant-initiated renewals
  • handle webhooks for real-time updates

Prerequisites

Before starting, ensure you have:
  • Zentra developer account (Create account)
  • your API keys from the dashboard
  • a webhook endpoint configured
  • a stable idempotency strategy in your backend

Payment Flow Overview

customer present -> your backend -> /api/v1/payments/charges
                 -> your backend -> /api/v1/payments/charges/{reference}/verify
                 -> your backend -> business state update
                 -> later renewals -> /api/v1/payments/charges (merchant_initiated)

Step 1: Tokenize a Payment Method

Tokenize the payment method so your backend does not need to store raw payment credentials. Reviewed public token routes:
  • POST /api/v1/payments/tokens/card
  • POST /api/v1/payments/tokens/bank-account
  • GET /api/v1/payments/customers/{customer_id}/tokens

Step 2: Create a Customer-Present Charge

Create the initial charge with a stable reference and an idempotency key.
async function createCharge(order, paymentTokenId) {
  return client.payments.charge({
    amount_minor: order.total_minor,
    customer_id: order.customer_id,
    currency: "NGN",
    payment_token_id: paymentTokenId,
    capture_mode: "customer_action_required",
    reference: `ORD_${order.id}`,
    idempotency_key: `charge_${order.id}`
  });
}

Step 3: Verify the Charge

Verify the final status before you mark the order as paid.
async function finalizeCharge(reference) {
  const verification = await client.payments.verify(reference);

  if (verification.status === "success") {
    await db.orders.update({
      where: { reference },
      data: {
        status: "paid",
        paid_at: new Date(),
        payment_method: verification.channel,
      },
    });
  }

  return verification;
}
For setup charges, verification is also the step that makes a saved payment token ready for later merchant_initiated renewals.

Step 4: Handle Webhooks

Set up webhooks for reliable payment confirmation and refunds.
app.post("/webhooks/zentra", async (req, res) => {
  const signature = req.headers["x-zentra-signature"];
  const isValid = client.webhooks.verify(req.body, signature);

  if (!isValid) {
    return res.status(401).send("Invalid signature");
  }

  const { event, data } = req.body;

  switch (event) {
    case "payment.success":
      await finalizeCharge(data.reference);
      break;
    case "payment.refunded":
      await handleRefund(data);
      break;
  }

  res.status(200).send("OK");
});

Step 5: Process Refunds

Use POST /api/v1/payments/refunds with a stable business reference and idempotency key.
const refund = await client.payments.refund({
  charge_reference: "ORD_123",
  amount_minor: 25000,
  reason: "customer_request",
  idempotency_key: "refund_ORD_123_partial_1",
});

Step 6: Run Recurring Renewals

After a verified customer-present setup charge, later renewals can reuse the same token:
const renewal = await client.payments.charge({
  amount_minor: invoice.amount_minor,
  customer_id: invoice.customer_id,
  payment_token_id: invoice.payment_token_id,
  reference: `INV_${invoice.id}`,
  capture_mode: "merchant_initiated",
  idempotency_key: `invoice_${invoice.id}`,
});
Recurring renewals should fail closed if the token was never primed by a verified customer-present charge.

Best Practices

  • keep all amounts in integer minor units
  • make every charge and refund idempotent
  • verify server-side before updating business state
  • treat webhooks as replay-safe and signature-verified
  • never store raw PAN or CVV in your own application

Next Steps

Payments Overview

Review the current charge, refund, and token contract.

Handling Webhooks

Build replay-safe event processing.