> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usezentra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Accepting Payments

> Current public payment flow for charges, verification, refunds, and recurring reuse

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.

<Warning>
  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.
</Warning>

## 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](https://api.usezentra.com/developer/signup))
* your API keys from the dashboard
* a webhook endpoint configured
* a stable idempotency strategy in your backend

## Payment Flow Overview

```text theme={null}
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.

<CodeGroup>
  ```javascript create-charge.js theme={null}
  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}`
    });
  }
  ```

  ```python create_charge.py theme={null}
  def create_charge(order, payment_token_id):
      return client.payments.charge({
          "amount_minor": order["total_minor"],
          "customer_id": order["customer_id"],
          "currency": "NGN",
          "payment_token_id": payment_token_id,
          "capture_mode": "customer_action_required",
          "reference": f"ORD_{order['id']}",
          "idempotency_key": f"charge_{order['id']}",
      })
  ```
</CodeGroup>

## Step 3: Verify the Charge

Verify the final status before you mark the order as paid.

<CodeGroup>
  ```javascript verify-charge.js theme={null}
  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;
  }
  ```

  ```python verify_charge.py theme={null}
  def finalize_charge(reference):
      verification = client.payments.verify(reference)

      if verification["status"] == "success":
          db.orders.update(
              where={"reference": reference},
              data={"status": "paid"},
          )

      return verification
  ```
</CodeGroup>

<Info>
  For setup charges, verification is also the step that makes a saved payment token ready for later `merchant_initiated` renewals.
</Info>

## Step 4: Handle Webhooks

Set up webhooks for reliable payment confirmation and refunds.

```javascript theme={null}
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.

```javascript theme={null}
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:

```javascript theme={null}
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}`,
});
```

<Warning>
  Recurring renewals should fail closed if the token was never primed by a verified customer-present charge.
</Warning>

## 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

<CardGroup cols={2}>
  <Card title="Payments Overview" icon="credit-card" href="/api-reference/payments/overview">
    Review the current charge, refund, and token contract.
  </Card>

  <Card title="Handling Webhooks" icon="webhook" href="/guides/handling-webhooks">
    Build replay-safe event processing.
  </Card>
</CardGroup>
