GatewayCore

Build Payments Into
Anything You Create

The SHASHURA Gateway API gives you programmatic access to Mobile Money collections, payouts and SACCO ledger data. RESTful, predictable and developer-friendly.

REST JSON HTTPS Webhooks MTN MoMo Airtel Money Postman Collection

Authentication

One base URL serves both sandbox and production. The X-Target-Environment header picks which. Every call (except /auth itself) needs a short-lived Bearer token plus three identifying headers.

AuthorizationBearer <access_token>
X-CLIENT-IDYour app's client ID
X-Target-Environmentsandbox | production
X-COUNTRYUG

Tokens are valid for 1 hour. Requesting a new one before expiry just hands back the same token with its remaining lifetime, so there's no need to track expiry yourself.

POST /auth
// Request body (or send client_id as the X-CLIENT-ID header instead)
{
  "client_id":     "{your_client_id}",
  "client_secret": "{your_client_secret}"
}

// Response (200 OK)
{
  "status": "success",
  "data": {
    "access_token": "3f9a...c1",
    "token_type":   "Bearer",
    "expires_in":   3600
  }
}

Integrate in Under 10 Minutes

Get your client ID and secret from your Merchant dashboard, request a token, then make your first sandbox call.

1
Register as a Merchant

Create a Merchant account at no cost. Sandbox access is instant.

2
Get Your Client ID & Secret

Find them under Settings → API Access, alongside the full interactive docs and a ready-made Postman collection for your app.

3
Request an Access Token

POST to /auth with your client credentials to get a Bearer token you can reuse for up to an hour.

4
Make Your First Call

POST to /requestToPay with a test phone number to trigger a sandbox collection prompt.

// Trigger a USSD PIN prompt on the customer's phone
curl -X POST https://sandbox.shashura.com/api/v1/requestToPay \
  -H "Authorization: Bearer {token}" \
  -H "X-CLIENT-ID: {client_id}" \
  -H "X-Target-Environment: sandbox" \
  -H "X-COUNTRY: UG" \
  -H "Content-Type: application/json" \
  -d '{
    "msisdn": "256770000000",
    "amount": 500,
    "reference": "ORDER-101"
  }'

// Response (202 Accepted)
{
  "status": "success",
  "data": {
    "tid": "SCI177175711",
    "message": "Payment request successfully pushed. Awaiting customer PIN entry."
  }
}
// Trigger a USSD PIN prompt on the customer's phone
$ch = curl_init('https://sandbox.shashura.com/api/v1/requestToPay');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $token,
        'X-CLIENT-ID: ' . $clientId,
        'X-Target-Environment: sandbox',
        'X-COUNTRY: UG',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'msisdn'    => '256770000000',
        'amount'    => 500,
        'reference' => 'ORDER-101',
    ]),
]);
$response = json_decode(curl_exec($ch));
// Send money to a mobile wallet (protected by an atomic lien on your balance)
curl -X POST https://sandbox.shashura.com/api/v1/disburse \
  -H "Authorization: Bearer {token}" \
  -H "X-CLIENT-ID: {client_id}" \
  -H "X-Target-Environment: sandbox" \
  -H "X-COUNTRY: UG" \
  -H "Content-Type: application/json" \
  -d '{
    "msisdn": "256701222333",
    "amount": 5000,
    "reference": "PAY-9902",
    "remarks": "Commission"
  }'

// Response (200 OK)
{
  "status": "success",
  "code": "TRANSACTION_COMPLETED",
  "data": {
    "tid": "WTH250825120501",
    "amount": 5000,
    "charge": 500,
    "status": "PENDING",
    "message": "Payout initiated. Waiting for MTN confirmation."
  }
}
// Send money to a mobile wallet (protected by an atomic lien on your balance)
$ch = curl_init('https://sandbox.shashura.com/api/v1/disburse');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $token,
        'X-CLIENT-ID: ' . $clientId,
        'X-Target-Environment: sandbox',
        'X-COUNTRY: UG',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'msisdn'    => '256701222333',
        'amount'    => 5000,
        'reference' => 'PAY-9902',
        'remarks'   => 'Commission',
    ]),
]);
$response = json_decode(curl_exec($ch));
// Airtel settles instantly (status "TS"); MTN stays PENDING until the
// callback fires. A re-used reference returns 409 DUPLICATE_REFERENCE.
// Reverse a settled collection back onto your own wallet. Only the
// principal you received is reversed — the fee is kept. Omit amount
// (or pass 0) to refund whatever remains; pass a smaller amount for a partial refund.
curl -X POST https://sandbox.shashura.com/api/v1/refund \
  -H "Authorization: Bearer {token}" \
  -H "X-CLIENT-ID: {client_id}" \
  -H "X-Target-Environment: sandbox" \
  -H "X-COUNTRY: UG" \
  -H "Content-Type: application/json" \
  -d '{
    "tid": "ORDER-101",
    "amount": 0,
    "reason": "Customer requested a refund",
    "send_to_customer": false
  }'

// Response (200 OK)
{
  "status": "success",
  "code": "REFUNDED",
  "data": {
    "tid": "ORDER-101",
    "amount_refunded": 487.5,
    "refunded_total": 487.5,
    "refundable_total": 487.5,
    "fully_refunded": true,
    "payout": null
  }
}
// Reverse a settled collection — amount omitted (or 0) refunds whatever remains
$ch = curl_init('https://sandbox.shashura.com/api/v1/refund');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $token,
        'X-CLIENT-ID: ' . $clientId,
        'X-Target-Environment: sandbox',
        'X-COUNTRY: UG',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'tid'              => 'ORDER-101',
        'amount'           => 0,
        'reason'           => 'Customer requested a refund',
        'send_to_customer' => false,
    ]),
]);
$response = json_decode(curl_exec($ch));
// Only your principal is reversed — our processing fee is never refunded,
// on a full or partial refund. Set send_to_customer=true to also push a
// real payout of the refunded amount back to the original msisdn.
// Look up by your own reference, or our tid, same identifier slot
curl -X GET https://sandbox.shashura.com/api/v1/enquiry/ORDER-101 \
  -H "Authorization: Bearer {token}" \
  -H "X-CLIENT-ID: {client_id}" \
  -H "X-Target-Environment: sandbox" \
  -H "X-COUNTRY: UG"

// Response (200 OK)
{
  "status": "success",
  "data": {
    "tid": "SCI177175711",
    "external_ref": "ORDER-101",
    "msisdn": "256770000000",
    "amount": 500,
    "state": "TS",
    "network_ref": "881290312",
    "created_at": "2026-08-25 12:05:01"
  }
}

// If state is "TF", data also carries a "failure" object — omitted entirely
// on PENDING/TS so existing integrations are unaffected:
{
  "failure": {
    "code": "INSUFFICIENT_FUNDS",
    "message": "Not enough balance on this mobile money account."
  }
}
// state: PENDING · TS (success) · TF (failed) — on TF, data.failure.code is
// one of: INSUFFICIENT_FUNDS, WRONG_PIN, CANCELLED_BY_USER, TIMED_OUT,
// LIMIT_EXCEEDED, ACCOUNT_ERROR, PROVIDER_ERROR, UNKNOWN
$ch = curl_init('https://sandbox.shashura.com/api/v1/enquiry/ORDER-101');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $token,
        'X-CLIENT-ID: ' . $clientId,
        'X-Target-Environment: sandbox',
        'X-COUNTRY: UG',
    ],
]);
$response = json_decode(curl_exec($ch));

Real-Time Payment Callbacks

Instead of polling, set a callback URL on your app (Settings → Apps) and we'll POST the result there the moment a transaction reaches a final state, usually within seconds. Each app has its own callback URL and signing secret.

transaction.finalized

One event covers both collections and payouts. Check the type (COLLECTION / DISBURSEMENT) and status (SUCCESSFUL / FAILED) fields in the payload.

POST to your callback URL
{
  "event":        "transaction.finalized",
  "environment":  "sandbox",
  "tid":          "SCI177175711",
  "external_ref": "ORDER-101",
  "type":         "COLLECTION",
  "status":       "SUCCESSFUL",
  "amount":       500,
  "charge":       12.5,
  "net_amount":   487.5,
  "msisdn":       "256770000000",
  "network":      "MTN",
  "timestamp":    "2026-08-25T12:05:03+03:00"
}

// If status is "FAILED", the payload also carries a "failure" object:
{
  "failure": {
    "code":    "INSUFFICIENT_FUNDS",
    "message": "Not enough balance on this mobile money account."
  }
}
PHP: verify the signature
$raw = file_get_contents('php://input');
$sig = hash_hmac('sha256', $raw, $yourWebhookSecret);

if (!hash_equals($sig, $_SERVER['HTTP_X_SHASHURA_SIGNATURE'] ?? '')) {
    http_response_code(401); exit; // reject forgeries
}

$event = json_decode($raw);
http_response_code(200); // acknowledge with any 2xx
Every delivery carries an X-Shashura-Signature header: an HMAC-SHA256 of the raw body keyed with your app's webhook secret (Settings → Apps). Always verify it before trusting the payload.

Failure codes (data.failure.code / failure.code)

INSUFFICIENT_FUNDS Not enough balance on the customer's mobile money account.
WRONG_PIN The customer entered an incorrect PIN.
CANCELLED_BY_USER The customer cancelled or rejected the payment prompt.
TIMED_OUT The customer did not respond to the prompt in time.
LIMIT_EXCEEDED This would exceed the account's mobile money transaction limit.
ACCOUNT_ERROR The mobile money account could not be reached (not found / not allowed).
PROVIDER_ERROR The mobile network operator reported an internal error — safe to retry.
UNKNOWN A decline reason the operator sent that we haven't classified yet.

Error Glossary

Every response carries a status field (and a code on errors). Treat that as the source of truth alongside the HTTP status.

400
INVALID_INPUT

Missing or malformed msisdn, amount, or reference.

401
UNAUTHORIZED / EXPIRED_TOKEN

Bearer token missing, invalid, or expired. Re-authenticate via /auth.

401
INVALID_SECRET / INVALID_APP

client_secret didn't match, or no app exists for that client ID + environment.

402
INSUFFICIENT_FUNDS

Available balance (actual minus pending-payout liens) is too low for this payout.

403
IP_NOT_WHITELISTED

Caller IP is not in this app's allow-list.

404
NOT_FOUND

No transaction matches the identifier you queried.

405
METHOD_NOT_ALLOWED

Wrong HTTP verb for this endpoint (e.g. GET on a POST-only route).

409
DUPLICATE_REFERENCE

This reference was already used for a transaction on this app.

500
Server Error

Unexpected error on our end, logged and investigated automatically.

Ready to start building?

Sign up for a free Merchant account to get sandbox credentials, the full interactive API reference, and a ready-made Postman collection.

Get API Keys