> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.transxact.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.transxact.io/_mcp/server.

# Integrate with an AI agent

This page is one linear recipe for adding Transxact payments to an app. It's
written so a coding agent (Claude Code, Cursor, Copilot and the like) can
follow it end to end, but it reads just as well for a person. Paste its URL
into your agent, or point the agent at
[`llms.txt`](https://docs.transxact.io/llms.txt) for the whole site. Every page
also has a Markdown version: add `.md` to its URL.

The shape of every Transxact integration:

1. Your server creates a **Checkout Session** for the order.
2. You redirect the Customer to the session's `hostedUrl`. Transxact takes the
   payment with M-PAiSA or MyCash.
3. Transxact sends your server a signed **webhook** with the outcome.
4. You fulfil the order from the webhook, not from the Customer's redirect.

## Before you start

* A Test mode secret key, `sk_test_...`, from
  [signing up](https://transxact.io/signup) with the Business account. Test
  mode simulates payments, so nothing here charges anyone. Webhooks need a
  Business account.
* A webhook signing secret, from the **Webhooks** section of the dashboard,
  once you've added an endpoint there. Each endpoint has its own.
* Put both in environment variables, `TRANSXACT_SECRET_KEY` and
  `TRANSXACT_WEBHOOK_SECRET`. Never in source code, a browser or a mobile app.

## 1. Install the SDK

```bash
npm install @transxact/node          # Node and TypeScript
composer require transxact/transxact # PHP
```

Any other language can call the HTTP API directly: see the
[API Reference](https://docs.transxact.io/api-reference) or the
[OpenAPI spec](https://docs.transxact.io/openapi.json).

## 2. Create a Checkout Session on your server

```typescript
import { TransxactApiClient } from "@transxact/node";

const transxact = new TransxactApiClient({
  token: process.env.TRANSXACT_SECRET_KEY!,
});

app.post("/checkout", express.json(), async (req, res) => {
  const order = await loadOrder(req.body.orderId); // your own lookup

  const session = await transxact.checkoutSessions.create({
    idempotencyKey: `order-${order.id}`,
    amount: order.totalCents, // FJD cents: 5000 is FJD 50.00
    currency: "FJD",
    successUrl: `https://shop.example/orders/${order.id}/thanks`,
    cancelUrl: "https://shop.example/cart",
    metadata: { orderId: String(order.id) },
  });

  await saveSessionId(order.id, session.id); // optional: metadata already links them
  res.redirect(303, session.hostedUrl);
});
```

* `amount` is an integer in FJD cents, under FJD 5,000.00 (at most `499999`).
* The idempotency key is required. Derive it from the order, so a retried
  request replays the same session instead of creating a second one.
* `metadata` comes back on every retrieve and webhook, so you can find the
  order without keeping your own mapping.

## 3. Send the Customer to `hostedUrl`

The redirect above is the whole step. The hosted page offers every Provider and
handles each one's own steps. Afterwards it sends the Customer to `successUrl`
or offers a way back to `cancelUrl`, with `?session_id=cs_...` appended.

Your `successUrl` page should say "thanks, we're confirming your payment",
not "paid". Anyone can type that URL. If the page needs the real outcome,
retrieve the session:

```typescript
const session = await transxact.checkoutSessions.retrieve({
  id: req.query.session_id as string,
});
// session.status: "pending" | "succeeded" | "failed" | "cancelled"
```

## 4. Receive and verify the webhook

```typescript
import { verifyWebhookSignature } from "@transxact/node/webhooks";

// Raw body, not express.json(): the signature covers the exact bytes.
app.post(
  "/webhooks/transxact",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const rawBody = req.body.toString("utf8");
    const ok = verifyWebhookSignature(
      rawBody,
      req.header("Transxact-Signature") ?? "",
      process.env.TRANSXACT_WEBHOOK_SECRET!,
    );
    if (!ok) return res.status(400).end();

    const event = JSON.parse(rawBody);
    if (await alreadyHandled(event.id)) return res.status(200).end();

    const session = event.data?.object;
    switch (event.type) {
      case "checkout_session.succeeded":
        await markOrderPaid(session.metadata.orderId, session.amount);
        break;
      case "checkout_session.failed":
      case "checkout_session.cancelled":
        await releaseOrder(session.metadata.orderId);
        break;
    }

    await recordHandled(event.id);
    res.status(200).end();
  },
);
```

In PHP, the same check is `Transxact\Webhooks::verifySignature($rawBody,
$signatureHeader, $secret)`. See [Verifying webhooks](/guides/verifying-webhooks).

* Answer `2xx` quickly. Anything else is retried for about a day, with the
  same event `id`.
* Delivery is at least once, so skip event `id`s you've already handled.
* The dashboard's **Send test** sends a signed `webhook.test` event.
  Return `2xx` for types you don't handle.

## 5. Fulfil from the webhook

Ship, unlock or mark the order paid only in the
`checkout_session.succeeded` handler, after checking the session's `amount`
matches the order. That's the only signal that money moved. `failed` and
`cancelled` are final for that session. To let the Customer try again, create
a new Checkout Session with a new idempotency key.

## 6. Test it end to end

1. Expose your server with a tunnel (`cloudflared tunnel --url
   http://localhost:3000` or `ngrok http 3000`) and set its `https://` address
   plus your webhook path as an endpoint in the dashboard.
2. Press **Send test** and check your endpoint returns `2xx`.
3. Start a checkout, open the `hostedUrl` and pay with the practice screens:
   **Approve payment** for M-PAiSA, or any 7-digit number such as `7123456`
   with code `123456` for MyCash.
4. Check the order is marked paid by the webhook, and try **Decline payment**
   for the failure path.

## 7. Go live

Once your Verification is approved, create a Live mode key on the dashboard
and swap `sk_test_...` for `sk_live_...`. Nothing else changes. Until a
Provider takes Live mode payments, creating a session returns `409
live_mode_unavailable`. See [Test vs Live mode](/guides/test-vs-live-mode).

## Common mistakes

* **Amounts in dollars.** `amount: 50` is 50 cents. FJD 50.00 is `5000`.
* **Fulfilling on the redirect.** The `successUrl` visit proves nothing.
  Fulfil from the verified `checkout_session.succeeded` webhook.
* **Secret key in the client.** `sk_` keys belong on your server only. A
  mobile app asks your server to create the session (see
  [Mobile integration](/guides/mobile-integration)).
* **Skipping signature verification**, or verifying re-serialized JSON
  instead of the raw body.
* **A random idempotency key per request.** A retry then creates a second
  session. Tie the key to the order.
* **Mixing modes.** A Test mode session can't be retrieved with a Live mode
  key, and the reverse. Each returns `404`.
* **Treating duplicate webhooks as new payments.** Deduplicate on the event
  `id`.
* **Branching on error messages.** Branch on `error.code`. `message` can
  change.