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

# Accept a payment with the API

> Build your own payment UI: pick a medium, render the next action, collect input, and confirm.

Use this path when you want full control of the payment screen. If you
don't need that, the [embedded checkout](https://docs.kwugwo.africa/checkout-embed.md) does all of
this for you.

> [!NOTE]
>
> All calls on this page use your **secret key**, so they run on your
> server. Your frontend talks only to your own backend.

## Overview

```mermaid theme={"system"}
sequenceDiagram
    participant B as Browser
    participant S as Your server
    participant K as Kwugwo

    S->>K: POST /v1/ugwo
    B->>S: Customer picks a medium (and a bank)
    S->>K: POST /v1/ugwo/{uid}/activities
    K-->>S: activity + next_action
    S-->>B: Render next_action
    opt input_otp / input_pin / …
        B->>S: Customer enters the code
        S->>K: PUT …/actions/{uac}
    end
    K->>S: Webhook ugwo.successful
    S-->>B: Order confirmed
```

## 1. Create the ugwo

When the customer reaches your payment step, create an ugwo for the
order and keep its `uid` with the order.

```js Node theme={"system"}
const ugwo = await kwugwo("POST", "/v1/ugwo", {
  amount: order.totalKobo,   // 500000 = ₦5,000.00
  currency: "NGN",
  ref: order.id,             // unique: makes retries safe
  description: `Order #${order.id}`,
  onye_email: order.email,
});
await db.orders.update(order.id, { ugwoUid: ugwo.uid });
```

The examples on this page use this small helper:

```js Node theme={"system"}
async function kwugwo(method, path, body) {
  const res = await fetch(`${process.env.KWUGWO_API}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${process.env.KWUGWO_SECRET_KEY}`,
      "Content-Type": "application/json",
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const json = await res.json();
  if (!res.ok) throw Object.assign(new Error(json.message ?? json.detail), { status: res.status, body: json });
  return json;
}
```

## 2. Show the available mediums

`ugwo.checkout.mediums` lists what the customer can use, in the order you
configured. For `ussd` and `pay_with_bank`, also fetch the banks they
can choose from:

```js Node theme={"system"}
const { providers } = await kwugwo("GET", `/v1/checkouts/${ugwo.checkout.uid}/ussd`);
// { "gtb": { "id": "gtb", "name": "Guaranty Trust Bank" }, ... }
const banks = Object.values(providers); // [{ id, name }, ...] for your picker
```

Send the chosen `id` as `provider`. Cache the list for a few minutes;
it only changes when you change PSPs or routing.

## 3. Charge with the chosen medium

```js Node theme={"system"}
const activity = await kwugwo("POST", `/v1/ugwo/${ugwo.uid}/activities`, {
  type: "charge",
  data: {
    medium: "ussd",
    medium_params: { provider: "gtb" },
  },
});
await db.orders.update(order.id, { activityUid: activity.uid });
```

| Medium | `medium_params` |
| - | - |
| `bank_transfer` | `{}` or `{ "expires_at": "<ISO-8601, ≤ 15 min ahead>" }` |
| `ussd` | `{ "provider": "gtb" }` |
| `pay_with_bank` | `{ "provider": "palmpay", "account_number": "8012345678", "account_name_fragment": "Okonkwo" }` (shows the customer the resolved account name to confirm; doesn't block the charge) |

Full rules: [Charge an ugwo](https://docs.kwugwo.africa/ugwo/create-activity.md#medium-parameters).

## 4. Render the next action

If `activity.status` is `requires_action`, render
`activity.next_action.action` by its `type`:

```js Node theme={"system"}
function render(nextAction) {
  const a = nextAction.action;
  switch (a.type) {
    case "user_action":
      // bank_transfer: show bank_name, account_number, account_name + countdown to expires_at
      // ussd: show a.headers.ussd as a big, tappable "tel:" link
      return { kind: "instructions", text: a.instruction, details: a.headers, expiresAt: nextAction.expires_at };

    case "open_url":
      // pay_with_bank: redirect, or iframe only if a.iframe === true
      return { kind: "redirect", url: a.url, canFrame: a.iframe, details: a.headers };

    case "input_otp":
    case "input_pin":
    case "input_bvn":
    case "input_birthday":
      return { kind: "form", field: a.type.replace("input_", ""), canResend: a.supports_resend === true };

    default:
      // A type you don't know yet: fall back to the embedded checkout
      return { kind: "fallback" };
  }
}
```

| `action.type` | Show the customer | Then |
| - | - | - |
| `user_action` | `instruction` and `headers` (account details or USSD code), with a countdown to `expires_at`. | Wait for the webhook. |
| `open_url` | A button to `url`. Embed in an iframe only if `iframe` is `true`. | Wait for the webhook. |
| `input_otp`, `input_pin`, `input_bvn`, `input_birthday` | A form for that one value. | [Submit it](#5-submit-customer-input). |

See [The Next action object](https://docs.kwugwo.africa/ugwo/next-action-object.md#action-types) for every field.

## 5. Submit customer input

For `input_*` actions, send what the customer typed straight to Kwugwo.
Don't log or store it.

```js Node theme={"system"}
const action = await kwugwo(
  "PUT",
  `/v1/ugwo/${ugwoUid}/activities/${activityUid}/actions/${nextAction.uid}`,
  { otp: req.body.otp },     // or { pin }, { bvn }, { birthday: "YYYY-MM-DD" }
);

if (action.status === "pending" && action.last_error === "invalid_input") {
  return showError("That code didn't work. Try again.");
}

// Accepted: the activity may now have a new next_action (e.g. OTP after PIN)
const activity = await kwugwo("GET", `/v1/ugwo/${ugwoUid}/activities/${activityUid}`);
return render(activity.next_action);
```

For `input_otp` with `supports_resend: true`, send `{ "otp": null }` to
resend the code.

## 6. Wait for the result

Payment completes asynchronously. Listen for webhooks on your server:

| Event | Meaning |
| - | - |
| `ugwo.successful` | Paid in full. [Validate and fulfil](https://docs.kwugwo.africa/guides/validate-and-fulfil.md). |
| `ugwo.activity.updated` with `data.status` `failed` / `expired` | The attempt ended. The ugwo is back in `requires_ugwo`; let the customer try again. |
| `ugwo.activity.next_action.created` | A new step (e.g. OTP after PIN). Refresh the screen. |

To update the customer's screen in real time, push these from your
server to the browser (WebSocket, SSE), or have the page poll **your**
backend, which reads [Get an activity](https://docs.kwugwo.africa/ugwo/get-activity.md), every few
seconds.

## 7. Retry after a failure

If the attempt fails or expires, start a new activity on the **same**
ugwo, with the same or a different medium. Read `activity.error.type`
to decide what to tell the customer:

| `error.type` | Suggest |
| - | - |
| `issuer_error` | "Your bank declined this payment. Try another bank or method." |
| `psp_error` | "We couldn't start this payment. Please try again." |
| `system_error` | "Something went wrong. Please try again." |

You don't have to wait for an attempt to expire: if the customer
changes their mind (say from bank transfer to USSD), create a new
activity straight away. Stop showing the old instructions, since a
customer who completes both attempts is [double
charged](https://docs.kwugwo.africa/ugwo/activity-object.md#double-charges). Once the ugwo is paid,
new attempts return `409 already_processed`.

If creating the activity itself fails with `503 payment_routing_error`,
no PSP could take the charge right now. Nothing was created and the
ugwo is still `requires_ugwo`: let the customer retry in a moment or
pick another medium.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.