Quickstart
This takes a mobile money collection from nothing to a completed payment in the
sandbox. No real money moves. Everything here works the same way against
api.paysigna.com when you are ready.
You need a test secret key. Get one from the merchant portal at
app.paysigna.com under Developers → API keys, in
the Test environment. It looks like psg_test_7hQ2mKpR9fL3xW8v_….
1. Check the key works
Section titled “1. Check the key works”curl https://sandbox.paysigna.com/v1/balances \ -H "Authorization: Bearer psg_test_7hQ2mKpR9fL3xW8v_…"A 200 with your balances means the key is good. A 401 means the key is wrong
or revoked; a 403 with environment_mismatch means you are holding a live key
against the sandbox host, or the reverse.
2. Find the network to charge
Section titled “2. Find the network to charge”Mobile money charges need a momo_network_id. It is a UUID, not a name, so
resolve it once and cache it rather than hardcoding it:
curl https://sandbox.paysigna.com/v1/reference/momo-networks \ -H "Authorization: Bearer psg_test_…"There are matching reference lists for banks, currencies and countries.
3. Create the charge
Section titled “3. Create the charge”The Idempotency-Key is required on this call. Generate a fresh UUID per
payment attempt and keep it — if this request times out, you resend it with the
same key and get the original answer rather than a second charge.
curl https://sandbox.paysigna.com/v1/charges \ -H "Authorization: Bearer psg_test_…" \ -H "Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7" \ -H "Content-Type: application/json" \ -d '{ "amount": 15000, "currency": "GHS", "channel": "mobile_money", "momo_network_id": "1f0a9c2e-5b31-4d8e-9a77-0c1b2d3e4f50", "customer": { "identifier": "0541234567", "name": "Ama Mensah" }, "reference": "order-1043", "description": "Order 1043" }'amount is minor units: 15000 is GHS 150.00. There is no decimal form of an
amount anywhere in this API.
Put whatever amount and whatever mobile number you like in it. The sandbox has no
amount ceiling and no list of approved test numbers or cards, so you can rehearse
with the figures your product actually charges. To make this charge fail instead,
add "metadata": { "sandbox_outcome": "insufficient_funds" } — the full list is in
Sandbox and testing.
The response:
{ "id": "txn_01JB3K2N8QZX4V7M9P0R5T6W8Y", "object": "charge", "status": "awaiting_action", "reference": "PSG-4K2N8QZX", "your_reference": "order-1043", "amount": 15000, "currency": "GHS", "customer_amount": 15000, "merchant_amount": 14700, "fee": { "platform": 300, "tax": 0, "bearer": "merchant" }, "channel": "mobile_money", "customer": { "masked": "054****567", "name": "Ama Mensah" }, "action": { "type": "prompt" }, "created_at": "2026-10-05T14:21:03Z", "expires_at": "2026-10-05T14:36:03Z"}Three things to read carefully:
statusisawaiting_action, and that is success. The customer has a prompt on their handset. Most charges returnpendingorawaiting_actionand complete asynchronously. Do not treat the absence ofsucceededas a failure — this is the single most common first-integration bug.action.typetells you what the customer has to do.promptmeans it is on their phone and you wait.3ds_redirectorhosted_checkoutcome with aurlyou send their browser to.merchant_amountis what lands in your balance —amountminus the fee. Reconcile against this, not againstamount.
Store the id against your order now, before you do anything else. It is how
every later message about this payment identifies itself.
4. Register a webhook endpoint
Section titled “4. Register a webhook endpoint”The status change arrives as a webhook. Register the URL once — this is setup, not per-payment:
curl https://sandbox.paysigna.com/v1/webhook-endpoints \ -H "Authorization: Bearer psg_test_…" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.example.com/paysigna/webhooks", "subscribed_events": ["transaction.succeeded", "transaction.failed", "transaction.expired"], "description": "Order status" }'Subscribe explicitly, as above. An empty subscribed_events means every event
type, which costs you deliveries and us retries for events you ignore.
The URL must be HTTPS and publicly routable — loopback and private addresses are refused, because our delivery workers run inside our own network. For local development use a tunnel.
5. Handle the webhook
Section titled “5. Handle the webhook”When the customer approves, you get a POST to that URL. Verify the signature
before you trust a byte of it, then enqueue and return quickly — we count a
response slower than your timeout_seconds (default 10) as a failed attempt.
{ "id": "evt_01JB3K2P4RT7YV9X1Z3B5D7F9H", "object": "event", "type": "transaction.succeeded", "resource_type": "transaction", "resource_id": "txn_01JB3K2N8QZX4V7M9P0R5T6W8Y", "sequence": 48213, "created_at": "2026-10-05T14:22:41Z", "data": { "...": "the transaction, as in step 3" }}Deduplicate on id — delivery is at-least-once by design. Record the highest
sequence you have processed; a gap in it means a lost event, and you can ask for
exactly the missing range later.
Webhooks has the verification code and the full argument for why it is shaped the way it is. Do not skip it.
6. Read the charge when you need to
Section titled “6. Read the charge when you need to”curl https://sandbox.paysigna.com/v1/charges/txn_01JB3K2N8QZX4V7M9P0R5T6W8Y \ -H "Authorization: Bearer psg_test_…"Use this to answer a question about a specific payment — a customer asking, a webhook you think you missed, a reconciliation run. Do not poll every open transaction on a timer: it burns your rate limit and still tells you later than the webhook does.
That is the whole flow
Section titled “That is the whole flow”Create, receive, read. A payout is the same three steps pointed the other way
(POST /v1/payouts), and a refund is POST /v1/refunds against a charge.
Next:
- Authentication — scopes, key types, and what to do about rotation before it surprises you.
- Idempotency — the rules, and what a retry actually does.
- Errors — the codes worth branching on.
- Going live — what has to be true before a
psg_live_…key works.

