SadadSD Integration Documentation
SadadSD is Sudan's national interoperability layer for digital merchant payments. This reference covers everything you need to integrate — whether you are a bank / mobile-banking provider or a merchant (e-commerce, POS, or app).
Bank Integration
Resolve QR codes, show payer confirmation, report payment result.
Merchant Integration
Create payments, display QR, poll status, receive webhooks.
Base URLs
| API | Base URL |
|---|---|
| Bank API | https://sadadsd.hithamhaidartech.com/api/v1/bank |
| Merchant API | https://sadadsd.hithamhaidartech.com/api/v1/merchant |
| Portal API | https://sadadsd.hithamhaidartech.com/api/v1/portal |
Version 1 is the supported contract. Unversioned paths are deprecated compatibility aliases; see the compatibility and retirement policy.
A PHP 8.1+ merchant/bank reference client and webhook verifier are available
under sdk/php. It implements exact-target signing, safe retries,
idempotency keys, bounded clock recovery, and delivery-ID replay protection.
All endpoints are served over HTTPS only. HTTP requests will be redirected.
Authentication
Both Bank and Merchant APIs use API key + secret header authentication. Credentials are issued by SadadSD and must be kept secret — never expose them in client-side code.
| Header | Value | Description |
|---|---|---|
X-API-Key | string | Your public API key — identifies your integration. |
X-API-Secret | string | Your secret — verified server-side with bcrypt. Treat like a password. |
X-SadadSD-Timestamp | unix seconds | Current Unix time. The production acceptance window is five minutes. |
X-SadadSD-Nonce | string | A cryptographically random, single-use value of 16–128 URL-safe characters. |
X-SadadSD-Signature | hex | Lowercase HMAC-SHA256 signature of the canonical request. |
Request signing
Build the canonical string exactly as METHOD + "\n" + REQUEST_TARGET + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + SHA256(RAW_BODY). The request target includes the path and query string exactly as sent. Compute a lowercase hexadecimal HMAC-SHA256 using your API secret. Sign the exact body bytes sent on the wire; never reuse a nonce.
Bank IP Whitelisting
Bank API keys may have an IP whitelist enforced. Requests from non-whitelisted IPs receive 403 Forbidden. Contact SadadSD to update your allowed IP ranges.
Example
curl -X POST https://sadadsd.hithamhaidartech.com/api/v1/bank/resolve \
-H "X-API-Key: bk_live_a1b2c3d4e5f6..." \
-H "X-API-Secret: sk_xxxxxxxxxxxxxxxx..." \
-H "Content-Type: application/json" \
-d '{"qr_data":"..."}'
Request Format
All write requests (POST) accept a JSON body with Content-Type: application/json. GET parameters are passed as query string parameters.
All endpoints also accept multipart/form-data where file uploads are involved.
Response Format
Every response is JSON with a consistent envelope:
{
"response": /* endpoint-specific payload, or false on error */,
"messages": [
{ "message": "Human-readable message", "type": "success" }
],
"time": "2026-08-09 14:23:01"
}
| Field | Type | Description |
|---|---|---|
response | any | false | The main payload. false on error. |
messages | array | One or more messages with type: success | error | warning | info. |
time | datetime | Server timestamp (Africa/Khartoum timezone, UTC+2). |
Error Codes
| HTTP Status | Meaning |
|---|---|
200 | Success. Check response for the payload. |
400 | Bad request — missing or invalid parameter. Read messages. |
401 | Authentication failed — invalid or missing API credentials. |
403 | Forbidden — IP not whitelisted, or insufficient role. |
404 | Resource not found. |
405 | Wrong HTTP method for this route. |
409 | Conflict — duplicate resource, replay, or resource state changed before the request completed. |
429 | Too many attempts (OTP retry limit). |
500 | Internal server error. |
Transaction States & Transitions
Every SadadSD transaction moves through the following states:
| State | Who sets it | Description |
|---|---|---|
requested | Merchant API | Payment created, QR generated, waiting to be scanned. |
risk_review | SadadSD (risk policy) | A configured risk rule quarantined the payment for administrator review; no QR is issued and the payment cannot be scanned until it is approved. |
risk_blocked | SadadSD (risk policy) | A configured risk rule blocked the payment; it is final and never becomes payable. |
scanned | Bank API — /resolve | Bank scanned the QR; payer confirmation screen is being shown. |
processing | Bank API — /processing | Bank atomically claimed the transaction and may now initiate the debit. |
success | Bank API — /result | Bank confirmed the payment was processed successfully. |
failed | Bank API — /result | Bank reported a payment failure (reason provided). |
cancelled | Merchant API — /cancel_payment | Merchant cancelled the transaction (QR no longer visible). |
expired | SadadSD (automatic) | Transaction reached expire_at without being completed. |
POST /cancel_payment whenever the QR is no longer visible to the customer — page navigation, session timeout, POS lock, or explicit cancel. Failure to cancel creates stale transactions that may confuse customers.risk_review (awaiting administrator approval — the response still includes the full intent/transaction detail plus an explainable risk decision) or blocks it as risk_blocked. A blocked payment is final; a reviewed payment resumes exactly where it stopped after approval.QR Code Format
The qr_data returned by POST /create_payment is a Base64-encoded, HMAC-SHA256 signed string with the format:
base64( "<uuid>|<expire_at>|<amount>|<hmac_sha256_signature>" )
The HMAC signature is computed with a server-side secret key — tampered or forged QR codes are rejected by POST /resolve with code: "INVALID_QR".
Encode this string into any QR code format (e.g. QR Code ISO/IEC 18004). For deep-link/inter-app flows pass it as a URL parameter.
Fee Model
SadadSD charges a fixed amount per successful transaction. The payer's confirmation screen must display all three amounts:
| Field | Description |
|---|---|
seller_amount | Amount due to the merchant (set by merchant when creating the payment). |
sadad_fee | SadadSD's fixed service fee (set per endpoint, not negotiable per transaction). |
total_payable | seller_amount + sadad_fee — the total the payer must approve. |
All amounts are in SDG (Sudanese Pound).
The endpoint fee is selected from SadadSD's approved effective-dated fee schedule when the payment request starts. That exact fee is stored on the payment intent and never changes afterward, including after a later fee version becomes active or when an idempotent request is replayed.
For each completed Africa/Khartoum business day, SadadSD produces immutable transaction, reconciliation-exception, and fee-summary CSV snapshots. Fee monetary totals include successful payments only; every status remains visible in the transaction file and status counters. Each file has a retained SHA-256 digest. A different authorized administrator may sign off only the latest unchanged revision and only after all non-final or open-reconciliation exceptions are resolved by the authoritative bank result flow. Finance review never changes a payment outcome.
Bank Integration Guide
Your mobile banking app backend calls the Bank API when a customer wants to pay using a SadadSD QR code.
- Customer opens your banking app and initiates payment The customer scans a SadadSD QR code displayed at a merchant checkout (website, POS screen, or printed).
-
Your app sends the QR data to POST /resolve
SadadSD verifies the QR signature, checks expiry, and returns full transaction details: merchant name, amounts, purpose. The transaction is marked
scanned. -
Show the payer confirmation screen
Display
merchant_name,seller_amount,sadad_fee, andtotal_payable. The payer must see and approve all three amounts. -
Payer confirms — claim processing before any debit
Call
POST /processing. Only if it returnsprocessingmay your backend execute funds movement. A409means the transaction was cancelled, expired, or changed; do not debit. -
Report the result to POST /result
Submit
status: "success"orstatus: "failed"with yourbank_referencenumber. SadadSD notifies the merchant via webhook.
Bank API Reference
Base URL: https://sadadsd.hithamhaidartech.com/api/v1/bank
All endpoints require X-API-Key and X-API-Secret headers.
Verify a QR code and retrieve full transaction details for the payer confirmation screen. Marks the transaction as scanned.
Request Body
| Parameter | Type | Description |
|---|---|---|
| qr_datarequired | string | Base64-encoded QR string from the QR code or deep-link. |
Response
{
"transaction_uuid": "550e8400-e29b-41d4-a716-446655440000",
"merchant_name": "Khartoum Electronics",
"merchant_logo": "https://...",
"payment_purpose": "Product Sale",
"endpoint_name": "Main Store",
"endpoint_type": "ecommerce",
"seller_amount": 500.00,
"sadad_fee": 10.00,
"total_payable": 510.00,
"currency": "SDG",
"expire_at": "2026-08-09 14:38:00",
"requested_at": "2026-08-09 14:23:00"
}
Error Codes
| code | Meaning |
|---|---|
INVALID_QR | Signature verification failed — QR was tampered or is malformed. |
EXPIRED | QR code has passed its expiry time. |
SCANNED / SUCCESS etc. | Transaction is no longer in requested state. |
Example
curl -X POST https://sadadsd.hithamhaidartech.com/api/v1/bank/resolve \
-H "X-API-Key: YOUR_BANK_API_KEY" \
-H "X-API-Secret: YOUR_BANK_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"qr_data":"dXVpZHxleHBpcmV8YW1vdW50fHNpZw=="}'
Atomically claim a scanned transaction immediately before initiating the debit. This resolves the cancellation race: a merchant may cancel while the payer is reviewing, but cancellation cannot win after the bank has acquired the processing claim.
status: "processing", do not move funds.Request Body
{ "transaction_uuid": "550e8400-e29b-41d4-a716-446655440000" }
Response
{ "transaction_uuid": "550e8400...", "status": "processing" }
Report the final payment outcome after successfully claiming /processing. Only the owning bank may report the result. This endpoint is idempotent — re-submitting the same result returns the stored status.
/resolve) can submit a result for that transaction. Attempts by other banks return 403.Request Body
| Parameter | Type | Description |
|---|---|---|
| transaction_uuidrequired | uuid | The UUID returned by /resolve. |
| statusrequired | string | "success" or "failed". |
| reasonoptional | string | Human-readable reason, especially for failures (e.g. "Insufficient balance"). |
| bank_referencerequired for success | string | Your bank's unique internal transaction reference number. |
Response
{ "transaction_uuid": "550e8400...", "status": "success" }
Example
curl -X POST https://sadadsd.hithamhaidartech.com/api/v1/bank/result \
-H "X-API-Key: YOUR_BANK_API_KEY" \
-H "X-API-Secret: YOUR_BANK_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"transaction_uuid":"550e8400-...","status":"success","bank_reference":"TXN-2026-88812"}'
Re-fetch status and amounts for a transaction that was previously scanned by your bank. Useful for reconciliation or re-sync after a connectivity issue.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| uuidrequired | uuid | The transaction UUID. |
Example
curl "https://sadadsd.hithamhaidartech.com/api/v1/bank/transaction?uuid=550e8400-..." \
-H "X-API-Key: YOUR_BANK_API_KEY" \
-H "X-API-Secret: YOUR_BANK_API_SECRET"
List stale processing exceptions owned by your authenticated bank. Poll the open queue, query your authoritative payment ledger, then retry POST /result with the original final status and bank reference. SadadSD never guesses whether an uncertain debit succeeded.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| statusoptional | string | open (default), resolved, or all. |
| pageoptional | integer | Page number, starting at 1. |
| limitoptional | integer | 1–100 cases per page. |
Disputes & Refunds
Merchant portal users can open a support case for their own final transaction. Cases, participant notes, internal support notes, and status events are separate from the immutable payment lifecycle. A case resolution never changes the original bank-reported result.
501 Not Implemented.Any future refund API will create a separate immutable, idempotent resource linked to the original success and will rely on a participating provider for funds movement and authoritative result reporting. It requires a new versioned contract, reconciliation behavior, provider certification, and regulator/legal approval. See REFUND_POLICY.md.
Merchant Integration Guide
Your server-side backend uses the Merchant API. Each registered endpoint (website, POS device, or app) has its own api_key + api_secret.
- Register and get approved via the Portal Create your merchant account at sadadsd.hithamhaidartech.com/portal, submit for review, and add your bank settlement account. Once approved, create an Endpoint to receive your API credentials.
-
Create a payment with POST /create_payment
Send the
amount(seller's amount in SDG). Receive aqr_datastring and expiry. -
Display the QR / launch the banking app
Encode
qr_dataas a QR code and display it. For inter-app flow, pass it as a deep-link parameter to the banking app. -
Poll GET /payment_status or receive a webhook
Poll every 2–3 seconds until status changes from
requested. Or use webhooks for server push. -
Cancel if the QR is abandoned
Call POST /cancel_payment when the QR is no longer visible. Never leave a
requestedtransaction unresolved.
Merchant API Reference
Base URL: https://sadadsd.hithamhaidartech.com/api/v1/merchant
All endpoints require X-API-Key and X-API-Secret headers for the specific endpoint (not the merchant account). Endpoints must have status active and their parent merchant must be approved.
Create a new payment intent. Returns a signed QR string and amount breakdown.
Request Body
| Parameter | Type | Description |
|---|---|---|
| amountrequired | number | Seller amount in SDG. Must be positive. Does not include the SadadSD fee. |
| purpose_idoptional | integer | ID from GET /purposes. Labels the transaction. |
| operator_idoptional | integer | ID from GET /operators. Identifies the POS cashier. |
| expire_minutesoptional | integer | QR validity in minutes. Range: 1–1440. Default: 15. |
| reference_internal_idoptional | integer | Your own order / invoice ID for reconciliation. |
| reference_internal_tableoptional | string | Table name the reference_internal_id belongs to (e.g. "orders"). |
Response
{
"transaction_uuid": "550e8400-e29b-41d4-a716-446655440000",
"qr_data": "dXVpZHxleHBpcmV8YW1vdW50fHNpZw==", // encode this as QR
"seller_amount": 500.00,
"sadad_fee": 10.00,
"total_payable": 510.00,
"currency": "SDG",
"expire_at": "2026-08-09 14:38:00",
"expire_minutes": 15
}
Example
curl -X POST https://sadadsd.hithamhaidartech.com/api/v1/merchant/create_payment \
-H "X-API-Key: YOUR_ENDPOINT_API_KEY" \
-H "X-API-Secret: YOUR_ENDPOINT_API_SECRET" \
-H "Idempotency-Key: order-9001-20260809" \
-H "Content-Type: application/json" \
-d '{"amount":500.00,"expire_minutes":10,"reference_internal_id":9001}'
Poll the current status of a transaction. Also auto-expires overdue requested transactions.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| uuidrequired | uuid | Transaction UUID from /create_payment. |
Response
{
"transaction_uuid": "550e8400-...",
"status": "success",
"status_reason": "",
"seller_amount": 500.00,
"sadad_fee": 10.00,
"total_payable": 510.00,
"currency": "SDG",
"scanned_by_bank": "Bank of Khartoum",
"bank_reference": "TXN-2026-88812",
"expire_at": "2026-08-09 14:38:00",
"requested_at": "2026-08-09 14:23:00",
"scanned_at": "2026-08-09 14:24:12",
"updated_at": "2026-08-09 14:25:03"
}
success, failed, cancelled, or expired.Cancel a pending transaction. Only cancellable while status is requested or scanned. This endpoint is idempotent.
Request Body
| Parameter | Type | Description |
|---|---|---|
| uuidrequired | uuid | Transaction UUID. |
| reasonoptional | string | Reason for cancellation. Default: "Cancelled by merchant". |
Example
curl -X POST https://sadadsd.hithamhaidartech.com/api/v1/merchant/cancel_payment \
-H "X-API-Key: YOUR_ENDPOINT_API_KEY" \
-H "X-API-Secret: YOUR_ENDPOINT_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"uuid":"550e8400-...","reason":"Customer closed checkout"}'
Register or update the webhook URL for this endpoint. SadadSD will POST transaction.success and transaction.failed events here. Send an empty webhook_url to remove the webhook.
Request Body
| Parameter | Type | Description |
|---|---|---|
| webhook_urloptional | url | HTTPS URL to receive events. Must resolve to a public IP. |
| webhook_secretoptional | string | 16–128 character secret for signature verification. |
Paginated list of transactions for this endpoint.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| statusoptional | string | all | Filter by status: requested | scanned | processing | success | failed | cancelled | expired |
| pageoptional | integer | 1 | Page number. |
| limitoptional | integer | 20 | Records per page. Max: 100. |
Response
{ "page": 1, "limit": 20, "total": 143, "transactions": [ /* array of transaction objects */ ] }
Returns the authenticated endpoint's configuration and its parent merchant's profile.
Returns active cashier operators for this merchant. Use to populate an operator selector in POS/cashier apps. Pass the selected operator_id to /create_payment.
Returns active payment purposes. Pass the selected purpose_id to /create_payment to label the transaction.
Webhooks
When a transaction reaches a terminal state, SadadSD sends an HTTP POST to your registered webhook_url.
Events
| Event | Triggered when |
|---|---|
transaction.success | Bank reports status: "success". |
transaction.failed | Bank reports status: "failed". |
Payload
{
"event": "transaction.success",
"data": {
"transaction_uuid": "550e8400-...",
"status": "success",
"reason": "",
"bank_reference": "TXN-2026-88812",
"seller_amount": 500.00,
"sadad_fee": 10.00,
"total_payable": 510.00
},
"timestamp": 1754749381
}
Delivery
- Timeout: 10 seconds. No automatic retry — use GET /payment_status as a fallback.
- Your endpoint must return a
2xxresponse. - Verify the signature before trusting the payload (see below).
Webhook Signature Verification
Every webhook delivery includes a X-SadadSD-Signature header. Verify it before processing the event.
X-SadadSD-Signature: sha256=a1b2c3d4e5f6...
X-SadadSD-Event: transaction.success
Verify by computing HMAC-SHA256 of the raw request body using your webhook_secret:
// PHP
$body = file_get_contents('php://input');
$secret = 'your_webhook_secret';
$expected = 'sha256=' . hash_hmac('sha256', $body, $secret);
$received = $_SERVER['HTTP_X_SADADSD_SIGNATURE'] ?? '';
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($body, true);
// Node.js
const crypto = require('crypto');
function verify(req, secret) {
const body = req.rawBody; // ensure you have raw body
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
const received = req.headers['x-sadadsd-signature'] ?? '';
return crypto.timingSafeEqual(
Buffer.from(expected), Buffer.from(received)
);
}
# Python
import hmac, hashlib
def verify(body: bytes, secret: str, header: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header)
hash_equals / timingSafeEqual / hmac.compare_digest) to prevent timing attacks.