Smobilpay S3P API v3.2.0
Smobilpay Third-Party API · S3P

Central Africa's Payment Infrastructure API.

One API to collect payments from customers, disburse payouts to recipients, and access a growing catalog of value-added services across mobile money and digital wallets in Africa.

Fundamentals

Key concepts

The S3P API uses a dynamic service model. Two top-level categories cover all use cases — but 90% of integrations only need one of them.

Collections & Disbursement

90% of integrations · 3-step flow
  • Collection — collect payment from a customer's mobile wallet via /cashout
  • Disbursement — disburse funds into a recipient's mobile wallet via /cashin
  • Static catalog — cache once at startup
  • Fewest steps, fastest to integrate

Value-added services

10% of integrations · 4-step flow
  • Bills — electricity, water, internet (per-customer lookup)
  • Top-ups — airtime and data (static catalog)
  • Vouchers — return a redeemable PIN on purchase
  • Subscriptions — recurring service payments

Vocabulary

Throughout this guide we use merchant-perspective business terms. Each one maps to a single catalog endpoint you call once at startup to discover the available payment packages.

Collection → GET /cashout
Money in to you. Customer pays you from their mobile wallet.
Disbursement → GET /cashin
Money out from you. You pay a recipient into their mobile wallet. Also called a payout.
PTN
Payment Transaction Number — Smobilpay's unique transaction ID. Store immediately.
payItemId
Identifies a specific payment package or bill item. Required for every quote.
quoteId
Single-use price confirmation. Expires quickly.
trid
Your custom reference. Enables idempotency and lookup by your own ID.
serviceNumber
The customer's or recipient's mobile wallet number at the provider.
XAF
System currency (Central African CFA franc). All accounts settle in XAF.
Security

Authentication

The S3P API uses standard OAuth 2.0 with the client_credentials grant. Trade your publicKey and secretKey for a short-lived JWT bearer token, then send it on every request — every standard HTTP client and every Authorization: Bearer …-aware tool works as-is.

🔒
Server-side credentials onlyNever embed your publicKey / secretKey in client-side code, mobile binaries, logs, or version control. Mint tokens server-side and forward only the short-lived bearer downstream.

OAuth 2.0 (client_credentials)

Two-step flow: exchange your publicKey and secretKey for a short-lived JWT access token at /oauth/token, then attach Authorization: Bearer … on every API request. Once minted, only the bearer travels with each call — your credentials stay on your server.

  1. Request a token

    Send publicKey:secretKey as HTTP Basic credentials. grant_type is optional; if present it must be client_credentials.

    POST {YOUR_API_HOST}/oauth/tokenhttp
    POST /oauth/token HTTP/1.1 Host: {YOUR_API_HOST} Content-Type: application/x-www-form-urlencoded Authorization: Basic <base64(publicKey:secretKey)> grant_type=client_credentials
    200 OKjson
    { "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 3600 }
    ℹ️
    Read expires_in off every responseToken TTL is set per-response — don't hard-code it. Refresh roughly 60s early to absorb clock skew.
  2. Attach as a bearer header

    examplehttp
    Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

    Send this header on every secured endpoint. The publicKey:secretKey pair stays out of the request — only the short-lived JWT travels.

  3. Refresh before expiry

    client_credentials tokens are not refreshable — request a new one when the current one expires. Track now + expires_in client-side and refresh ~60s early.

ℹ️
Access tokens are reusable until expThat's the OAuth 2.0 contract (RFC 6749 §1.4). Cache the bearer and reuse it until ~60s before exp — don't mint a fresh one per request.
Collections & Disbursement

Collections & Disbursement

The primary integration category. Collect payments from customers' mobile wallets, or disburse funds to recipients. Payment packages are static — cache once, then quote → execute → verify.

Collection · money in

Accept a payment

Money flows out of the customer's wallet and into your Maviance account.

GET · ONCE
/cashout
discovery endpoint
Disbursement · money out

Make a payout

Money flows from your Maviance account into the customer's wallet.

GET · ONCE
/cashin
discovery endpoint
💡
Quick reference: /cashout = the Collection catalog (money in to you). /cashin = the Disbursement catalog (money out from you). Both are GET discovery endpoints — call once, cache the payItemId values, then quote and execute against them. You don't need to think about the names again after that.
How it works

Watch the flow run end to end.

Two flows, same four-step pattern: quote, execute, customer step, callback. Each stage below shows the customer device on the left and your server's HTTP traffic on the right. Press Run flow to step through it. The default outcome path is callback-driven — we POST the final status to the URL configured on your account; polling /verifytx is only the fallback if a callback hasn't arrived within your SLA.

Collection

money in
Customer pays you. Their phone shows an approval prompt; you wait for the result.
Customer approves
Customer declines
Times out
Insufficient funds
  1. 1
    Quote the price
    POST /quotestd
  2. 2
    Execute the collection
    POST /collectstd · returns PTN
  3. 3
    Customer approves on phone
    Mobile money provider · push prompt
  4. 4
    Receive callback
    POST {your callback URL}
PTN IDLE waiting for callback
9:41
9:41
Tuesday, May 7
🔒 Slide up to unlock
Mobile Money
Payment request
Approve XAF 10,000 for Acme Co. — tap to review.
Confirm payment
XAF 10,000
to Acme Co.
From•••• 0001
FeesXAF 0
Cancel
Confirm
Enter your PIN
1
2
3
4
5
6
7
8
9
0
Payment sent
XAF 10,000
PTN ·
Mobile Money
You cancelled the request to pay Acme Co. XAF 10,000. No funds were debited.
Mobile Money
Your approval window expired. Ask the merchant to resend if you still want to pay.
Mobile Money
Insufficient balance. Top up your wallet and try again.
Quote
Execute
Verify
// Step 1 — get a price quote POST /v2/quotestd Content-Type: application/json { "payItemId": "S-1-10-CMMTNMOMO-90001-500100-1", "amount": 10000 } // → 200 OK { "quoteId": "Q-8821-cde456", "priceLocalCur": 10000, "localCur": "XAF", "expiresIn": 120 }
// Step 2 — execute the collection POST /v2/collectstd { "quoteId": "Q-8821-cde456", "serviceNumber": "237700000001", "customerPhonenumber": "237700000001", "trid": "order-00123" } // → 200 OK — store PTN immediately { "ptn": "99999164…34567890", "status": "PENDING", "trid": "order-00123" }
// Step 4 — receive the callback (default path) // We POST the final status to the callback URL on your account. POST {your-callback-url} Content-Type: application/json { "status": "SUCCESS", "settlementId": "S-44129", "trid": "order-00123" } // Acknowledge with 2xx within ~5s; non-2xx is retried. // Fallback — only if no callback arrives within your SLA: // GET /v2/verifytx?trid=order-00123 (same response shape) // ✓ Now safe to fulfil the order.

Payout

money out
You send funds to a recipient. They don't approve — they receive a credit notification when it lands.
Settles successfully
Invalid wallet
Insufficient agent balance
Provider unavailable
  1. 1
    Quote the payout
    POST /quotestd · with payout payItemId
  2. 2
    Execute the disbursement
    POST /collectstd · returns PTN
  3. 3
    Provider routes to recipient
    No customer approval needed
  4. 4
    Receive callback
    POST {your callback URL}
PTN IDLE waiting for callback
9:41
9:41
Tuesday, May 7
🔒 Slide up to unlock
Mobile Money
Incoming transfer
XAF 25,000 received from Acme Payouts.
Funds received
+ XAF 25,000
PTN ·
Mobile Money
No account found for that wallet number. Verify the recipient details.
Smobilpay
Agent balance below required amount. Top up to enable payouts.
Mobile Money
Provider gateway unreachable. Retry with backoff.
Quote
Execute
Verify
// Step 1 — quote a payout (payItemId from /cashin) POST /v2/quotestd { "payItemId": "S-2-10-CMMTNMOMO-90001-700200-1", "amount": 25000 } // → 200 OK { "quoteId": "Q-9103-fgh789", "priceLocalCur": 25000, "localCur": "XAF" }
// Step 2 — execute the payout POST /v2/collectstd { "quoteId": "Q-9103-fgh789", "serviceNumber": "237680000002", // recipient wallet "customerPhonenumber": "237680000002", "trid": "payout-ref-456" } // → 200 OK { "ptn": "99999176…77821554302", "status": "PENDING", "trid": "payout-ref-456" }
// Step 4 — receive the callback (default path) // We POST the final settlement to your configured callback URL. POST {your-callback-url} Content-Type: application/json { "status": "SUCCESS", "settlementId": "S-44211", "trid": "payout-ref-456" } // Acknowledge with 2xx within ~5s; non-2xx is retried. // Fallback — only when reconciling before a retry: // GET /v2/verifytx?trid=payout-ref-456 (idempotency safety) // ✓ Recipient credited.
Transaction state machine
Every payment lives in one of four states. Hover an edge to see what triggers the transition.
IDLE no transaction yet POST /collectstd PENDING awaiting outcome customer approves · funds settle decline · timeout · provider error SUCCESS fulfil order now ERRORED

The flow above is the same primitive you'll use for collections, payouts, and most VAS calls — only the discovery endpoint and the package's payItemId change. Read the accept-a-payment and send-a-payout guides for full request/response details.

Collection · money in

Accept a payment money flows out of customer's wallet

Four steps: get a quote using a cached package from /cashout, execute the collection, wait for the customer to approve on their phone, then receive a callback with the final status. GET /verifytx is the fallback if no callback arrives within your SLA.

GET · once/cashout POST/quotestd POST/collectstd POST · callbackyour URL · GET · fallback/verifytx
  1. Cache collection packages (once at startup)

    Fetch available collection packages. This is a static catalog — cache it and refresh periodically.

    GET /v2/cashoutjson
    [{ "payItemId": "S-1-10-CMMTNMOMO-90001-500100-1", "amountLocalCur": null, // null = variable amount "localCur": "XAF", "name": "MTN Mobile Money Collection" }]
  2. Request a quote

    Use the cached payItemId and the amount the customer will pay.

    ⏱️
    Quotes expire and are single-useExecute the collection immediately — the quoteId expires after a short window.
    POST /v2/quotestdjson
    { "payItemId": "S-1-10-CMMTNMOMO-90001-500100-1", "amount": 10000 } // Response { "quoteId": "Q-8821-cde456", "priceLocalCur": 10000, "localCur": "XAF" }
  3. Execute the collection

    Submit the quoteId and the customer's mobile wallet number. Smobilpay sends an approval prompt to their phone. Store the returned PTN immediately.

    POST /v2/collectstdjson
    { "quoteId": "Q-8821-cde456", "serviceNumber": "237700000001", "customerPhonenumber": "237700000001", "trid": "order-00123" } // Response — store PTN immediately { "ptn": "99999164577717700069821234567890", "status": "PENDING" }
  4. Receive the callback (and fall back to polling if needed)

    Once the customer approves on their phone, Smobilpay POSTs the final status to the callback URL configured on your account. Acknowledge with a 2xx within ~5 seconds; non-2xx responses are retried. If a callback hasn't arrived within your SLA window, fall back to GET /verifytx?trid=<your-trid> for reconciliation (rate limit: at most once every 10 seconds per transaction).

    🚨
    Only fulfil the order on SUCCESSPENDING means the customer has not yet approved. Never release goods or credit until you see SUCCESS.

    Transaction status

    StatusMeaningAction
    PENDINGAwaiting customer approvalWait for callback (or poll as fallback)
    SUCCESSCustomer approved — funds receivedFulfil order
    ERROREDDeclined, timed out, or wrong PINCheck errorCode, notify customer
    REVERSEDRare - Reversed during reconciliationContact support
Disbursement · money out

Make a payout money flows into recipient's wallet

A payout moves funds from your Maviance account to a recipient's mobile wallet using /cashin packages. No customer approval needed — funds land immediately on success and Smobilpay POSTs the final status to your callback URL.

GET · once/cashin POST/quotestd POST/collectstd POST · callbackyour URL · GET · fallback/verifytx
💡
Idempotency is critical for payoutsA network error does not mean the payout failed. Before retrying, always call GET /verifytx?trid=... to check if the original payout succeeded. Never pay a recipient twice.

The flow mirrors collections, just using /cashin packages. Quote with the payout amount, execute with the recipient's wallet, then receive the callback when funds settle. Fall back to GET /verifytx?trid=... for reconciliation or before any retry. Payouts typically settle faster than collections.

POST /v2/collectstd — payout examplejson
{ "quoteId": "Q-9103-fgh789", "serviceNumber": "237680000002", // recipient's wallet "customerPhonenumber": "237680000002", "trid": "payout-ref-456" }
Value-added services

Value-added services

Bill payments, top-ups, vouchers, and subscriptions. These require a per-customer lookup step before quoting — the payable item is dynamic per customer or service number.

TypeEndpointLookup frequencyNotes
SEARCHABLE_BILLGET /billPer customer, on demandReturns all open bills
NON_SEARCHABLE_BILLGET /billPer customer, on demandReturns a single bill item
TOPUPGET /topupAt startup / periodicStatic catalog
VOUCHERGET /voucherAt startup / periodicReturns redeemable PIN on purchase
PRODUCTGET /productAt startup / periodicStatic catalog
SUBSCRIPTIONGET /subscriptionPer customer, on demandTied to a customer account

VAS flow

GETdiscovery if isVerifiable/verify POST/quotestd POST/collectstd POST · callbackyour URL · GET · fallback/verifytx
⚠️
Bill payItemIds expireUnlike static catalog items, payItemId values from the bill lookup can become invalid if too much time passes before quoting. Complete the flow promptly.
Reference

Endpoint reference

All endpoints require a valid Authorization header.

Collections & Disbursement

GET/cashoutCollection packages

Returns available payment packages for accepting money from customers. Cache at startup, refresh periodically.

ParameterRequiredDescription
merchantoptionalFilter by merchant code (e.g. CMMTNMOMO)
serviceidoptionalFilter by service ID
GET/cashinPayout packages

Returns available packages for disbursing funds to recipients. Cache at startup, refresh periodically.

Value-added services

GET/billCustomer bill lookup

Returns open bills for a customer's account. Each result includes a payItemId for quoting. Called on-demand per customer.

ParameterRequiredDescription
merchantrequiredMerchant code
serviceidrequiredService ID
serviceNumberrequiredCustomer's account/meter number
GET/topupAirtime & data packages

Returns available airtime and data top-up packages. Cache at startup.

GET/productDigital product catalog

Returns the fixed product catalog. Cache at startup.

GET/voucherVouchers (return a PIN)

Returns available vouchers. A successful collection returns a redeemable PIN in the pin field of the POST /collectstd response.

GET/subscriptionCustomer subscriptions

Returns subscriptions for a customer account. Each has its own payItemId. Called on-demand per customer.

Shared

GET/pingHealth check
200 OKjson
{ "status": "OK", "version": "3.2.0" }
GET/accountAccount balance

Returns your current agent balance in XAF. Insufficient balance blocks all collections and payouts (error 40201). Monitor regularly.

GET/merchantList merchants

Returns all available merchants. Cache and refresh daily — new providers are added without API changes.

GET/serviceList services for a merchant

Returns services under a merchant. The serviceType tells you whether it's a collection, payout, bill, etc. Check isVerifiable for pre-validation support.

GET/verifyCheck that a service number is valid

For services where isVerifiable: true, returns a boolean indicating whether the supplied serviceNumber is valid for the selected service and merchant. Lightweight existence check — does not return customer identity data. Reduces failed transactions when used before /quotestd.

GET/validateResolve account holder details restricted

Validates an account (destination + serviceId) against the upstream service provider and, when available, returns the associated CustomerAccount including the customer's name. Unlike /verify, this endpoint returns personally identifiable information.

🔒
Restricted endpoint. Access is granted only to partners who have cleared Maviance's internal validation and compliance review (KYC / data-protection obligations apply to the returned customer name). Unauthorized callers receive 401. Contact your integration manager to request enablement.
POST/quotestdRequest a price quote

Used by all service types. Returns the exact cost and a single-use quoteId. Execute immediately — quotes expire quickly.

FieldRequiredDescription
payItemIdrequiredFrom /cashin, /cashout, or VAS discovery
amountrequiredTransaction amount in local currency
POST/collectstdExecute a payment

Executes any payment type. Returns a PTN — store immediately. The serviceNumber is the customer's wallet (collections) or recipient's wallet (payouts).

FieldRequiredDescription
quoteIdrequiredSingle-use, from POST /quotestd
serviceNumberrequiredCustomer wallet, recipient wallet, or provider account
customerPhonenumberconditionalRequired for most collections and payouts
tridoptionalYour unique reference — enables idempotency
GET/verifytxTransaction status (callback fallback)

Get the current status of any payment by PTN or your trid. Use this as a fallback when a callback hasn't arrived within your SLA, or before any retry to confirm idempotency. Don't poll faster than once every 10 seconds per transaction.

GET/historystdTransaction history

Paginated transaction history. Only one filter parameter per request (error 40011 if multiple).

Reference

Error codes

All HTTP errors return a JSON object. Use respCode for logic, devMsg for debugging, usrMsg is safe to surface to customers.

error response formatjson
{ "respCode": 4009, "devMsg": "Access token invalid", "usrMsg": "Authentication error", "link": "<support-url>" }

Authentication

CodeHTTPMeaningFix
4000400Authorization header malformedReview header format
4001400Authorization header missingAdd Authorization header
4005400Nonce already usedGenerate fresh unique nonce per request
4009400Access token invalid / bearer rejectedMint a fresh token via POST /oauth/token; verify credentials in the admin portal.

Account & limits

CodeHTTPMeaningFix
40201400Insufficient account balanceTop up agent account
40202400Collection limit exceededContact support to raise limit
60020400Outside configured work hoursRetry during work hours
40011400Multiple filter params on /historystdUse only one filter parameter

Execution errors

These appear in POST /collectstd responses as validation errors before the payment is processed.

CodeMeaningFix
4204Invalid phone/wallet number formatCorrect format and retry
40305Destination blocked (suspected fraud)Contact support
40609payItemId already used or invalidRe-fetch payItemId; restart flow
40611Service under maintenanceRetry later
40408Service does not support /verifyCheck isVerifiable before calling
41004Provider gateway unavailableRetry with exponential backoff
44001AML single-transaction limitReduce transaction amount

Payment errors (in errorCode)

These appear in callbacks or GET /verifytx responses when status: "ERRORED".

CodeMeaningCustomer-facing action
703201Approval prompt timed outAsk customer to retry
703202Customer declinedNotify customer; offer retry
703203Wrong PIN/OTPAsk customer to retry
703107Customer wallet insufficient fundsInform customer to top up
703103Wallet blockedCustomer should contact provider
702102Amount below provider minimumIncrease amount
702103Amount above provider maximumDecrease or split
703113Bill already paidRestart from bill discovery
5000Internal server errorRetry with exponential backoff
Libraries

SDKs & libraries

Six official Maviance clients (v3.2.0) cover the partner API end-to-end with OAuth 2.0 client_credentials built in — token mint, refresh, and Authorization: Bearer … attachment are handled for you. All six share the same endpoint coverage and a common smoke-test harness so cross-language behaviour stays in lock-step. If your language isn't listed, any standard HTTP client works as-is once you mint a token at POST /oauth/token.

PHPOfficial
PHP 8.2+ · PSR-18 HTTP client
composer require maviance/smobilpay-php-client
↗ github.com/maviance/smobilpay-php-client
PythonOfficial
Python 3.9+ · httpx + pydantic
pip install smobilpay-s3p-client
↗ github.com/maviance/smobilpay-python
JavaOfficial
JDK 17+ · Maven Central
org.maviance:smobilpay-java-client:3.2.0
↗ github.com/maviance/smobilpay-java
Node.jsOfficial
Node 18+ · zero runtime dependencies
npm install @maviance/smobilpay-s3p-client
↗ github.com/maviance/smobilpay-s3p-client-nodejs
Dart / FlutterOfficial
Dart 3.0+ · server & Flutter
dart pub add smobilpay
↗ github.com/maviance/smobilpay-dart
GoOfficial
Go 1.22+ · standard library net/http
go get github.com/maviance/smobilpay-go
↗ github.com/maviance/smobilpay-go
Guide

Testing your integration

Use the staging environment to validate your full integration before requesting production access.

Staging environment

SettingValueNotes
Base URLissued during onboardingMaviance shares the staging base URL once your account is provisioned
CredentialsSeparate staging token & secretNever use production credentials on staging
DataTest merchants onlyNo real money moves
ℹ️
Get staging accessOpen a ticket with Maviance support to receive your staging credentials and base URL.

Testing checklist

  • Authentication worksGET /ping returns 200
  • Collection packages loadGET /cashout returns payItemId values
  • Disbursement packages loadGET /cashin returns payItemId values
  • Collection full flow — quote → execute → callback → SUCCESS
  • Payout full flow — quote → execute → callback → SUCCESS
  • Callback fallback worksGET /verifytx?trid=... returns the same final status when callback is unreachable
  • Customer decline handled — error 703202 shows correct message
  • Idempotency worksGET /verifytx?trid=... before any retry
  • PTN is stored — DB records PTN immediately on execution response

Common mistakes

  • Crediting on PENDING — never fulfil until verifytx returns SUCCESS.
  • Duplicate payouts — a network error doesn't mean failure. Always check /verifytx?trid=... first if recovering from a network/timeout error
  • Hard-coded token TTL — read expires_in off every /oauth/token response and refresh ~60s before expiry. Hard-coding a value (e.g. 3600) breaks if the TTL is ever shortened.
  • Bearer rejected (4009) — mint tokens only via POST /oauth/token. Bearers obtained any other way will not authenticate.
  • Clock drift — JWT exp is checked against the server clock. Hosts with >5 min NTP drift may see tokens rejected as expired sooner than expected.
Integration pattern

Callbacks & transaction status

After executing a payment via POST /collectstd, the transaction enters PENDING. Smobilpay resolves it asynchronously. The recommended pattern is to wait for a callback, falling back to polling only if it doesn't arrive or if the network request encountered an interruption/timeout.

Recommended · Callback

Primary mechanism — low latency, low load
  • Register a webhook URL with Maviance (once)
  • After POST /collectstd, wait for Smobilpay to push the final status to your server
  • Process SUCCESS or ERROR immediately on receipt
  • If no callback arrives within your timeout window, fall back to polling

Fallback · Polling

Use only when callback doesn't arrive
  • Poll GET /verifytx?ptn=... at most every 10 seconds
  • Stop polling as soon as status is no longer PENDING
  • Do not poll continuously — violations can trigger account suspension
  • Store the PTN before starting the poll loop
  • Note that we are monitoring polling behavior to prevent abuse
🚨
Production API is monitoredDo not poll GET /verifytx faster than once every 10 seconds per transaction. Excessive polling can result in your account being suspended without warning.

Webhook setup

Webhooks are an opt-in feature. To enable, contact the Maviance ops team and provide the following:

FieldRequiredDescription
URLrequiredPublicly accessible HTTPS endpoint. If your server is behind a firewall, whitelist Smobilpay's IP addresses (request the list from ops).
SecretoptionalShared secret used to sign every callback. Strongly recommended — without it the X-Signature header is empty and you cannot verify authenticity.
Skip SSL VerificationoptionalSet true only if your endpoint uses a self-signed certificate. Not recommended for production.
🔒
Always configure a secret in productionThe secret is what lets you prove the callback came from Smobilpay. Without it, anyone who guesses your URL can forge status updates.

Delivery format

When a transaction reaches a final state (SUCCESS, ERRORED, or REVERSED), Smobilpay sends an HTTP POST with a JSON body to your configured URL. Delivery is at-least-once: transient failures are retried with exponential backoff and jitter (default 5 attempts).

Request headers

HeaderDescription
X-DeliveryUUIDv4 generated per delivery. Stable across automatic retries of the same delivery, so you can deduplicate without parsing the body. Format: 36 chars, [0-9a-f-].
X-PtnThe PTN of the transaction whose status changed. Match this against your stored PTN.
X-SignatureLowercase-hex HMAC-SHA1 of the raw request body, keyed with your configured secret. 40 chars of [0-9a-f]. Empty string if no secret is configured — treat empty as "unsigned", not "invalid".
Content-TypeAlways application/json; charset=UTF-8 exactly.
AcceptAlways application/json.

Request body

Field order on the wire is fixed: timestamp, trid, errorCode, status. All four are always present (no omitempty). Producers and merchants are advised to normalize strings to Unicode NFC before UTF-8 encoding so the HMAC-SHA1 signature is stable across implementations.

FieldDescription
timestampPayment time as YYYY-MM-DD HH:MM:SS (no T separator, no timezone suffix — typically UTC at Smobilpay). Not RFC 3339; legacy contract.
tridYour reference passed to POST /collectstd. Empty string if you didn't supply one.
errorCodeNumeric error code as a string. "0" on SUCCESS (and on PENDING), "3" on REVERSED, a non-zero domain-specific code on ERRORED. See Error codes.
statusOne of SUCCESS, ERRORED, REVERSED, PENDING (PENDING callbacks are rare — payment is still in flight).

Example callback

POST /your-webhook-endpointhttp
POST /exampleEndpoint HTTP/1.1 Host: your-server.example.com Content-Type: application/json; charset=UTF-8 Accept: application/json X-Delivery: fe6f0b85-0b3a-4f62-9f89-d7b3a18a8b18 X-Ptn: 99999152778369900057856272351928 X-Signature: 4ba0a40bff4fc5370f45e29f42a7a07bffacee26 {"timestamp":"2018-05-31 16:21:40","trid":"13550","errorCode":"0","status":"SUCCESS"}

Verifying the signature

Recompute the HMAC-SHA1 hex digest of the raw request body using your configured secret, then compare it to X-Signature. Use a constant-time comparison to prevent timing attacks.

⚠️
Sign the raw body bytesDo not deserialize-then-reserialize the JSON before signing — whitespace and key order would change and the signature would no longer match. Capture the raw body before parsing.

Reference example

signature inputstext
// Raw body to sign (exact bytes — no whitespace, fixed field order): {"timestamp":"2018-05-31 16:21:40","trid":"13550","errorCode":"0","status":"SUCCESS"} // Secret: secret // HMAC-SHA1 hex digest: 4ba0a40bff4fc5370f45e29f42a7a07bffacee26

Verification snippets

Node.js Python PHP Java Go
Express — capture raw bodyjavascript
const express = require('express'); const crypto = require('crypto'); const app = express(); // Capture raw bytes — DO NOT use express.json() before signing. app.use(express.raw({ type: 'application/json' })); app.post('/webhook', (req, res) => { const raw = req.body; // Buffer const received = req.get('X-Signature') || ''; const expected = crypto .createHmac('sha1', process.env.SMOBILPAY_WEBHOOK_SECRET) .update(raw) .digest('hex'); if ( received.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected)) ) { return res.status(401).send('invalid signature'); } const body = JSON.parse(raw.toString()); const ptn = req.get('X-Ptn'); const delivery = req.get('X-Delivery'); // Idempotency: skip if delivery already processed if (alreadyProcessed(delivery)) return res.sendStatus(200); handleStatusChange(ptn, body.trid, body.status, body.errorCode); markProcessed(delivery); res.sendStatus(200); // 2xx tells Smobilpay it was received });
Flaskpython
import hmac, hashlib, os from flask import Flask, request, abort app = Flask(__name__) SECRET = os.environ["SMOBILPAY_WEBHOOK_SECRET"].encode() @app.post("/webhook") def webhook(): raw = request.get_data() # bytes received = request.headers.get("X-Signature", "") expected = hmac.new(SECRET, raw, hashlib.sha1).hexdigest() if not hmac.compare_digest(received, expected): abort(401) body = request.get_json(force=True) ptn = request.headers["X-Ptn"] delivery = request.headers["X-Delivery"] if already_processed(delivery): return "", 200 handle_status_change(ptn, body["trid"], body["status"], body["errorCode"]) mark_processed(delivery) return "", 200
PHPphp
$raw = file_get_contents('php://input'); $received = $_SERVER['HTTP_X_SIGNATURE'] ?? ''; $expected = hash_hmac('sha1', $raw, getenv('SMOBILPAY_WEBHOOK_SECRET')); if (!hash_equals($expected, $received)) { http_response_code(401); exit('invalid signature'); } $body = json_decode($raw, true); $ptn = $_SERVER['HTTP_X_PTN']; $delivery = $_SERVER['HTTP_X_DELIVERY']; if (already_processed($delivery)) { http_response_code(200); exit; } handle_status_change($ptn, $body['trid'], $body['status'], $body['errorCode']); mark_processed($delivery); http_response_code(200);
Spring Bootjava
@PostMapping("/webhook") public ResponseEntity<String> webhook( @RequestHeader("X-Signature") String received, @RequestHeader("X-Ptn") String ptn, @RequestHeader("X-Delivery") String delivery, @RequestBody byte[] raw, @Value("${smobilpay.webhook.secret}") String secret ) throws Exception { Mac mac = Mac.getInstance("HmacSHA1"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA1")); StringBuilder hex = new StringBuilder(); for (byte b : mac.doFinal(raw)) hex.append(String.format("%02x", b)); String expected = hex.toString(); if (!MessageDigest.isEqual(expected.getBytes(), received.getBytes())) { return ResponseEntity.status(401).build(); } // parse, deduplicate by delivery, dispatch... return ResponseEntity.ok().build(); }
net/httpgo
func webhook(w http.ResponseWriter, r *http.Request) { raw, _ := io.ReadAll(r.Body) received := r.Header.Get("X-Signature") mac := hmac.New(sha1.New, []byte(os.Getenv("SMOBILPAY_WEBHOOK_SECRET"))) mac.Write(raw) expected := hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(received), []byte(expected)) { http.Error(w, "invalid signature", 401) return } var body struct { Timestamp string `json:"timestamp"` Trid string `json:"trid"` Status string `json:"status"` ErrorCode string `json:"errorCode"` } json.Unmarshal(raw, &body) delivery := r.Header.Get("X-Delivery") if alreadyProcessed(delivery) { w.WriteHeader(200) return } handleStatusChange(r.Header.Get("X-Ptn"), body.Trid, body.Status, body.ErrorCode) markProcessed(delivery) w.WriteHeader(200) }

Receiver checklist

  • HTTPS only — accept callbacks over TLS; reject plain HTTP.
  • Verify the signature on every request before doing anything else. Use constant-time comparison.
  • Sign the raw body — capture bytes before any JSON parsing or middleware.
  • Deduplicate by X-Delivery — UUID is stable across automatic retries within one delivery, so dedup is safe without parsing the body. For logical idempotency across requeues, also key on (X-Ptn, trid).
  • Match by X-Ptn or trid against your stored transaction record.
  • Return 2xx fast — any 2xx is treated as success and stops retries. Return 5xx, 408, or 429 if you want the delivery retried; other 4xx responses are recorded for audit but not retried. Acknowledge within a few seconds, then process asynchronously if work is heavy.
  • Always keep polling as a fallback — if a callback does not arrive within your SLA, fall back to GET /verifytx. Service providers occasionally experience service delays or interruptions, which can cause delays in fullfillment on the Smobilpay platform.
Guide

Going to production

Going live requires completing the Partner Integration Process — a structured onboarding journey managed jointly by you and Maviance.

Partner integration process

  1. Kick-off workshop

    API presentation, overview of functions, Q&A.

  2. Test account setup

    Maviance sets up your partner account on the test environment and shares test credentials.

  3. Integration

    You perform the integration. Maviance provides live support.

  4. Validation

    Maviance validates and certifies that the integration meets requirements.

  5. Go live

    Maviance sets up your partner account on production and shares production credentials.

⚠️
Pre-requisites for go-liveSuccessful validation of the implementation · commercial contract signed · KYC / due diligence completed.

Certifying your integration

Before production access is granted, Maviance performs a full User Acceptance Test in four stages.

  1. Implementation completed — partner finishes implementation.
  2. Partner self-certification UAT — provide logfiles and a completed test scenario document.
  3. Partner provides Maviance access — integration in test system, Maviance support as needed.
  4. Maviance final UAT — functional compliance to business processes & technical compliance to API restrictions.

Integration policies

During certification Maviance verifies compliance with the following policies. Violations in production can result in automatic account suspension without warning.

EndpointReasonRecommended call frequency
GET /accountAccount balance is returned in the payment collection response — no need to constantly re-request it.On demand
GET /verifytxFallback only — use when a callback hasn't arrived within your SLA, or before any retry for idempotency. Don't call excessively for the same payment.≤ once / 10s per tx
Master data
/merchant, /service, /cashin, /cashout, /topup, /product, /voucher
Master data does not change often and should be cached.Daily

Production security checklist

  • HTTPS only — never transmit bearer tokens or credentials over HTTP in production.
  • Credentials stay server-side — never expose your publicKey/secretKey (or a minted bearer) in client code, mobile binaries, or version control.
  • Store every PTN immediately — write PTN to DB before responding to the customer.
  • Idempotent retries via trid — always call GET /verifytx?trid=... before retrying.
  • Callbacks over polling — never faster than every 10 seconds per transaction.
  • Cache master data — refresh once daily.
  • Monitor account balance — set low-balance alerts; error 40201 blocks all payments.
  • Rotate credentials regularly — support zero-downtime rotation via the admin portal.
🚀
Ready to go live or need support?Open a ticket with Maviance support — your account manager will share production URLs and credentials.