---
name: waychit-payments
description: Integrate Waychit, the Gambia payment gateway, into any web app. Use when the user mentions Waychit or asks to accept payments in The Gambia (Wave, Afrimoney, QMoney, Yonna, NAFA, APS, Ecobank, Visa, Mastercard, Amex, UnionPay, Apple Pay, Google Pay). Covers payment requests, card payment sessions, retrieval, and signed webhooks.
---

# Waychit Payments Skill

Source of truth: https://waychit.com/developers

Waychit is a hosted payment gateway. Your server creates a payment, receives a `waychitLaunchUrl`, redirects the customer there, then Waychit notifies your server with a signed webhook when the payment completes.

- Base URL: `https://api.waychit.com`
- Auth: `Authorization: Bearer <WAYCHIT_API_KEY>` (server-side only)
- Headers: `Content-Type: application/json`, `Accept: application/json`
- Currency: GMD. Minimum amount: 5.
- There is no sandbox. Every request with a live key can move real funds.

## Setup order (follow exactly)

1. Create an `orders` table: `id`, `client_reference` (unique), `amount`, `status` (`pending` | `paid` | `failed` | `expired`), `waychit_id`, `metadata`, timestamps.
2. Build and deploy the webhook endpoint first (public, no auth, reads the raw body).
3. Tell the user to paste the webhook URL into the Waychit merchant dashboard, then copy the webhook signing secret and API key.
4. Only then store secrets: `WAYCHIT_API_KEY`, `WAYCHIT_WEBHOOK_SECRET`.
5. Build the server endpoint that creates the payment.
6. Wire the frontend button: call your server, then `window.location.href = waychitLaunchUrl`.

## Hard rules

- Never expose `WAYCHIT_API_KEY` to the browser, never log it, never commit it.
- Insert the order row with a generated `clientReference` (UUID) BEFORE calling Waychit.
- Validate `amount >= 5` on client and server.
- Never trust the redirect URL as proof of payment. Only the verified webhook (or a server-side retrieve call) marks an order paid.
- Webhooks: verify signature on the RAW body, reject timestamps older than 5 minutes, respond 2xx immediately, dedupe by event `id`, update by `clientReference` or `metadata.orderId`.
- Waychit retries non-2xx webhooks for up to 24 hours. Webhooks may be duplicated or missing, so reconcile with the retrieve endpoints.

## 1. Create a payment request (any method)

`POST /v1/payment-requests`

```json
{
  "amount": 200,
  "description": "benachin",
  "clientReference": "4c8e2b9f5d7a1e3c6b0f8a2d9e4b7c1f",
  "successRedirectUrl": "https://example.com/success",
  "failureRedirectUrl": "https://example.com/failure",
  "metadata": { "orderId": "order-1234" }
}
```

| Field | Type | Notes |
|---|---|---|
| amount | number | Required, GMD, min 5 |
| description | string | Optional |
| clientReference | string | Your order id, returned in the webhook |
| successRedirectUrl | string | Required |
| failureRedirectUrl | string | Required |
| metadata | object | Optional key/value, returned in the webhook |

Response: `{ success, message, paymentRequest: { id, amount, currency, status, waychitLaunchUrl, expiresAt, metadata, ... } }`. Status: `active` | `closed` | `expired` | `cancelled`.

## 2. Create a card payment session (cards only)

`POST /v1/payment-sessions/card`. Use only when the user explicitly wants card checkout for customers without the Waychit app.

```json
{
  "clientReference": "a9f3d1c84b2e7f0065cd9a1e3f8b4c72",
  "customerEmail": "customer@example.com",
  "returnRedirectUrl": "https://example.com/payments/success",
  "failureRedirectUrl": "https://example.com/payments/fail",
  "lineItems": [
    { "productName": "Domoda", "productDescription": "A delicious peanut soup.", "quantity": 1, "price": 750 },
    { "productName": "Yassa Ganarr", "quantity": 2, "price": 1500 }
  ],
  "metadata": { "orderId": "ORD-12345" }
}
```

Response: `{ success, message, paymentSession: { id, totalAmount, currency, status, waychitLaunchUrl, ... } }`.

## 3. Retrieve status

- `GET /v1/payment-requests/{id}` returns `paymentRequest`
- `GET /v1/payment-sessions/card/{id}` returns `paymentSession`

## 4. Webhooks

Header: `Waychit-Signature: t=<unix>,v1=<hex>,v1=<hex>` (several `v1` values during secret rotation).
Expected signature: `HMAC_SHA256(WAYCHIT_WEBHOOK_SECRET, "${t}.${rawBody}")` as hex. Accept if it matches ANY `v1`.

Events: `payment.request.completed`, `payment.session.completed`.

```json
{
  "id": "924341419659952",
  "type": "payment.request.completed",
  "data": {
    "id": "01M1M78FTB5RAAT4DFJMBYRSNE",
    "amount": 5,
    "currency": "GMD",
    "paymentStatus": "succeeded",
    "paymentRequestStatus": "closed",
    "clientReference": null,
    "metadata": { "orderId": "1234" },
    "transactionReference": "924341419659952",
    "paidDate": "2026-09-03T18:08:26.593Z",
    "paymentError": null
  }
}
```

Session events use `totalAmount`, `paymentSessionStatus`, and `customerEmail`.

## Reference server code (TypeScript, Web Crypto, works on Node 20+, Workers, Deno)

```ts
// create payment
export async function createWaychitPayment(order: { id: string; amount: number; description?: string }, origin: string) {
  if (order.amount < 5) throw new Error("Minimum amount is 5 GMD");
  const res = await fetch("https://api.waychit.com/v1/payment-requests", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.WAYCHIT_API_KEY}`,
      "Content-Type": "application/json",
      Accept: "application/json",
    },
    body: JSON.stringify({
      amount: order.amount,
      description: order.description ?? null,
      clientReference: order.id,
      successRedirectUrl: `${origin}/checkout/success?order=${order.id}`,
      failureRedirectUrl: `${origin}/checkout/failed?order=${order.id}`,
      metadata: { orderId: order.id },
    }),
  });
  const data = await res.json();
  if (!res.ok || !data.success) throw new Error(data.message ?? "Waychit error");
  return data.paymentRequest.waychitLaunchUrl as string;
}

// verify webhook
const hex = (buf: ArrayBuffer) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");

export async function verifyWaychitSignature(header: string | null, rawBody: string, secret: string) {
  if (!header) return false;
  const parts = header.split(",").map((p) => p.trim());
  const t = parts.find((p) => p.startsWith("t="))?.slice(2);
  const sigs = parts.filter((p) => p.startsWith("v1=")).map((p) => p.slice(3));
  if (!t || !sigs.length) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
  const expected = hex(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${t}.${rawBody}`)));
  return sigs.includes(expected);
}

export async function handleWaychitWebhook(req: Request) {
  const rawBody = await req.text(); // raw string, never re-stringify
  const ok = await verifyWaychitSignature(req.headers.get("Waychit-Signature"), rawBody, process.env.WAYCHIT_WEBHOOK_SECRET!);
  if (!ok) return new Response("Invalid signature", { status: 400 });
  const event = JSON.parse(rawBody);
  // Process async: dedupe on event.id, find order by data.clientReference ?? data.metadata?.orderId,
  // set status "paid" when data.paymentStatus === "succeeded", else "failed".
  queueMicrotask(() => processEvent(event));
  return new Response("ok", { status: 200 });
}
declare function processEvent(e: unknown): Promise<void>;
```

## 5. Government Service API (pay GovPay revenue types from your wallet)

Docs: https://waychit.com/developers/government-services. Wallet debit = amount + fee - discount. Fee: 5 up to 1,000, 10 up to 10,000, 15 above.

1. `GET /v1/government-service/revenue-types`. If a type has `amount`, use it (omit `amount` on payment). If `amount` is null, ask the customer and call `GET /v1/government-service/fee?amount=<whole GMD>`.
2. Show the total, then `POST /v1/government-service/payments` with `revenueType`, `revenueCode`, `companyOrPersonName`, `customer { name, phoneNumber }`, optional `amount`, `clientReference`, and always an `idempotencyKey`.
3. Statuses: `pending` (retry same key), `succeeded`, `failed` (not debited), `refunded`. Give the customer `assessmentNo` and `paymentTaxNumber`.
4. Retrieve: `GET /v1/government-service/payments/:id` or `?clientReference=`. Errors: 401 key, 400 balance/type/amount mismatch, 404 not found.

## Errors

- 400 `{ "success": false, "message": "Minimum amount to purchase is 5." }`
- 400 on sessions above the maximum amount
- 500 server error: show a retry message, do not mark the order failed.

## Copy rules for user-facing UI

- Button label: "Pay with Waychit". Link the word Waychit to https://waychit.com where shown.
- Phone inputs default to +220.

## Payouts API

Docs: https://waychit.com/developers/payouts. API key needs the `payouts` scope. Server-side only.

1. `POST /v1/payouts` with header `Idempotency-Key` (new key per payout, reuse to retry; different body with same key returns 409).
   Body: `channel` (wave | afrimoney | qmoney | yonna | aps | bank), `amount` (positive whole GMD), `destination` (`phoneNumber` as +220 plus 7 or 9 digits, or `accountNumber` for bank, optional `name`), optional `clientReference`, `reason` (max 40 chars), `metadata` (up to 20 keys).
2. Store `data.id`. Balance is debited `totalDebit = amount + fee`. 201 on create, 200 on safe retry.
3. Track `data.status` via `GET /v1/payouts/:id`, `GET /v1/payouts?clientReference=`, or webhooks `payout.succeeded`, `payout.failed`, `payout.reversed` (event `id` is stable, ignore repeats).
4. Statuses: pending, processing, succeeded, failed (refunded once), reversed (refunded once). Errors: 400, 401, 403 (missing scope), 404, 409, 429.
