Skip to navigation

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

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 or the OpenAPI spec.

2. Create a Checkout Session on your server

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:

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

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.

  • Answer 2xx quickly. Anything else is retried for about a day, with the same event id.
  • Delivery is at least once, so skip event ids 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.

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).
  • 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.