Quickstart

Quickstart — create a payment intent and receive webhooks

This walks a merchant developer through the tracer-bullet flow: create a

payment intent, drive it to completion in test mode, and receive the signed

webhook.

1. Get a test API key

Sign up (basic CDD) and create a test secret key, or a publishable key if you

only need to create intents. Authenticate requests with:


Authorization: Bearer <key>

2. Register a webhook endpoint

The aggregator will POST signed payment events to this URL.


POST /webhook-endpoints
Authorization: Bearer sk_test_...
Content-Type: application/json

{ "url": "https://you.example.com/hooks/transxact" }

The response returns the secret **once** — store it. An unverified endpoint

is marked active only after it answers the webhook.verified event with a

2xx.

3. Create a payment intent


POST /payment-intents
Authorization: Bearer sk_test_...
Idempotency-Key: order-1234
Content-Type: application/json

{ "amount": 2500, "reference": "order-1234", "return_url": "https://you.example.com/thanks" }

The intent starts in requires_action and returns a next_action:


{ "intent": { "id": "pi_...", "status": "requires_action", "next_action": { "type": "redirect", "url": "/simulate/pi_..." } } }

4. Drive it to completion (test mode)

In test mode every rail is simulated; the outcome is forced by a magic value.

**Redirect rails** (M-PAiSA, card) — open the next_action.url, or simulate it:


POST /simulate/pi_...

| Input | Outcome |

| --- | --- |

| reference: "test_success" or amount 1 | succeeded |

| reference: "test_fail" or amount 2 | failed |

| reference: "test_pending" | processing, then succeeds on the next simulate |

**MyCash** — create with "payment_method": "mycash" to get a collect_otp

next_action, then confirm the mobile and OTP server-side:


POST /payment-intents/pi_.../confirm   { "mobile": "+6797000000" }
POST /payment-intents/pi_.../confirm   { "otp": "123456" }

In test mode any OTP approves except the magic 000000 (which fails the

intent); the magic mobile +6790000000 is rejected.

5. Verify the webhook

You receive payment.processing then payment.succeeded (or payment.failed).

Verify the signature:


Transxact-Signature: v1=<hex>
Transxact-Timestamp: <unix-seconds>
X-Transxact-Event-Id: evt_...

The signature is HMAC-SHA256("<timestamp>.<body>") with your webhook secret.

Reject any delivery whose timestamp is older than 5 minutes.

Full contract: docs/api/openapi.yaml (the single source of truth, ADR-0006).

Live mode

An intent is created in the mode of the key that authenticates it: a live

secret/publishable key produces a mode: "live" intent; a test key produces

a mode: "test" intent.

values inside the aggregator, no real money, no external rail call.

generateAuth → handshake → hosted-page redirect; the aggregator verifies

the tokenv2 redirect callback before polling requeststatus to terminal

and posting the ledger capture.

sendOTPapprovePayment server-to-server flow on confirm.

Live mode requires the rail credentials — see scripts/setup-mpaisa.sh and

scripts/setup-mycash.sh.

Payment links

A **single-use link** has a fixed amount and is consumed on its first

successful payment; a **reusable "pay me" link** accepts a free amount with an

optional suggested amount.


POST /payment-links
Authorization: Bearer sk_test_...
Content-Type: application/json

{ "type": "single_use", "amount": 2500, "reference": "inv-1" }

The response's url is the hosted checkout. Opening it presents the shopper a

vertical method list (M-PAiSA, MyCash, card) and drives the payment in test

mode; a successful payment lands on a branded confirmation. A single-use link

refuses a second visit after it is used.