Skip to content

API overview

The gateway API is the one you integrate against. Your own servers call it with an API key, server to server. It is the only PaySigna surface that moves money on request, which is why it is also the only one that takes an idempotency key.

Two other APIs are documented on this site — the one the merchant portal calls and the one the staff console calls. Both authenticate with a browser session rather than an API key, and neither is meant for your integration. If you are wiring PaySigna into a product, everything you need is here.

Environment Host Keys Money
Sandbox https://sandbox.paysigna.com psg_test_… None.
Live https://api.paysigna.com psg_live_… Real.

They run the same code. The sandbox is not a mock: it is the gateway, with one substitution at the very end — the rail that would move the money. Everything before it is identical, so a flow that works in sandbox works live, which is the whole reason to have one.

The sandbox takes any amount, any mobile number, any account and any card, and you choose what it does. See Sandbox and testing.

api.paysigna.com is the only host you integrate against in production. There is no separate webhooks host — inbound partner callbacks terminate on the same origin under a path.

Four headers matter. Only the first is needed on a read.

Header On What it does
Authorization: Bearer psg_test_… Every request Your secret key. See Authentication.
Idempotency-Key Every state-changing request Makes a retry a replay instead of a second charge. See Idempotency.
Content-Type: application/json Any request with a body The API speaks JSON only.
PaySigna-Version Optional Pins the response shape. Reserved and documented ahead of the first breaking change; not yet honoured.

Amounts are always minor units and a currency — {"amount": 15000, "currency": "GHS"} is 150.00 cedis. There is no decimal form of an amount anywhere in the API, in either direction. This is not a stylistic preference: a float cannot hold 0.1 exactly, and a payments API that accepts one has agreed to be wrong about money occasionally.

Every response carries X-Request-Id, including every error. Log it next to your own record of the request. It is the one string that lets support find your exact call, and “it failed around 14:20” is not.

Successful responses are the resource. Errors are RFC 9457 Problem Details with a stable machine-readable code you can branch on:

{
"type": "https://docs.paysigna.com/api/errors/#insufficient_funds",
"title": "Insufficient funds",
"status": 402,
"detail": "The customer's wallet balance is below the requested amount.",
"code": "insufficient_funds",
"request_id": "01JB3K2N8QZX4V7M9P0R5T6W8Y"
}

Validation failures from the framework use the same shape, so there is one error format to parse rather than two. The full vocabulary is in Errors — and the codes are a published contract, so changing one is a breaking change on our side, not a refactor.

A complete collection is three endpoints:

  1. Create the charge. You get back a transaction with a status, and often an action for the customer to complete — a mobile money prompt on their handset, a 3-D Secure redirect.
  2. Receive the webhook when the status changes. This is the authoritative signal, and the one to build on.
  3. Read the transaction if you need to reconcile, or if you missed a webhook.

Payouts are the same three steps in the other direction.

What you should not build is a polling loop as the primary mechanism. The webhook arrives when the customer approves, which can be seconds or minutes; polling every transaction wastes your rate limit and still tells you later than the webhook would. Read the transaction when you need an answer about a specific one, not on a timer across all of them.