Developer Documentation
Guides Bank API Merchant API Portal →
API v1.0

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).

ℹ️
SadadSD does not move funds. It provides a secure interoperability layer — the bank/wallet partner executes the actual transfer and reports the result back via POST /result.

Base URLs

APIBase URL
Bank APIhttps://sadadsd.hithamhaidartech.com/api/v1/bank
Merchant APIhttps://sadadsd.hithamhaidartech.com/api/v1/merchant
Portal APIhttps://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.

HeaderValueDescription
X-API-KeystringYour public API key — identifies your integration.
X-API-SecretstringYour secret — verified server-side with bcrypt. Treat like a password.
X-SadadSD-Timestampunix secondsCurrent Unix time. The production acceptance window is five minutes.
X-SadadSD-NoncestringA cryptographically random, single-use value of 16–128 URL-safe characters.
X-SadadSD-SignaturehexLowercase HMAC-SHA256 signature of the canonical request.
⚠️
Rotate your secret immediately if you suspect it has been compromised. Use Portal → Endpoints → Rotate Keys for merchant credentials, or contact SadadSD for bank credentials.

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.

⚠️
The abbreviated curl samples below focus on endpoint payloads. Production integrations must add all three dynamic signing headers described above.

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"
}
FieldTypeDescription
responseany | falseThe main payload. false on error.
messagesarrayOne or more messages with type: success | error | warning | info.
timedatetimeServer timestamp (Africa/Khartoum timezone, UTC+2).

Error Codes

HTTP StatusMeaning
200Success. Check response for the payload.
400Bad request — missing or invalid parameter. Read messages.
401Authentication failed — invalid or missing API credentials.
403Forbidden — IP not whitelisted, or insufficient role.
404Resource not found.
405Wrong HTTP method for this route.
409Conflict — duplicate resource, replay, or resource state changed before the request completed.
429Too many attempts (OTP retry limit).
500Internal server error.

Transaction States & Transitions

Every SadadSD transaction moves through the following states:

REQUESTED → SCANNED → PROCESSING → SUCCESS
PROCESSING → FAILED
REQUESTED → CANCELLED   SCANNED → CANCELLED
REQUESTED → EXPIRED  (auto, on expire_at)
StateWho sets itDescription
requestedMerchant APIPayment created, QR generated, waiting to be scanned.
risk_reviewSadadSD (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_blockedSadadSD (risk policy)A configured risk rule blocked the payment; it is final and never becomes payable.
scannedBank API — /resolveBank scanned the QR; payer confirmation screen is being shown.
processingBank API — /processingBank atomically claimed the transaction and may now initiate the debit.
successBank API — /resultBank confirmed the payment was processed successfully.
failedBank API — /resultBank reported a payment failure (reason provided).
cancelledMerchant API — /cancel_paymentMerchant cancelled the transaction (QR no longer visible).
expiredSadadSD (automatic)Transaction reached expire_at without being completed.
⚠️
Cancellation obligation: Merchants must call 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 quarantine: SadadSD may evaluate a new payment or bank scan against configurable amount, velocity, merchant/provider/device, and anomaly rules. A match either returns the payment as 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:

FieldDescription
seller_amountAmount due to the merchant (set by merchant when creating the payment).
sadad_feeSadadSD's fixed service fee (set per endpoint, not negotiable per transaction).
total_payableseller_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.

  1. 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).
  2. 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.
  3. Show the payer confirmation screen Display merchant_name, seller_amount, sadad_fee, and total_payable. The payer must see and approve all three amounts.
  4. Payer confirms — claim processing before any debit Call POST /processing. Only if it returns processing may your backend execute funds movement. A 409 means the transaction was cancelled, expired, or changed; do not debit.
  5. Report the result to POST /result Submit status: "success" or status: "failed" with your bank_reference number. 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.

POST /resolve

Verify a QR code and retrieve full transaction details for the payer confirmation screen. Marks the transaction as scanned.

Request Body

ParameterTypeDescription
qr_datarequiredstringBase64-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

codeMeaning
INVALID_QRSignature verification failed — QR was tampered or is malformed.
EXPIREDQR 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=="}'
POST /processing

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.

🔒
Never debit first. If this endpoint does not return status: "processing", do not move funds.

Request Body

{ "transaction_uuid": "550e8400-e29b-41d4-a716-446655440000" }

Response

{ "transaction_uuid": "550e8400...", "status": "processing" }
POST /result

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.

🔒
Only the bank that scanned the QR (called /resolve) can submit a result for that transaction. Attempts by other banks return 403.

Request Body

ParameterTypeDescription
transaction_uuidrequireduuidThe UUID returned by /resolve.
statusrequiredstring"success" or "failed".
reasonoptionalstringHuman-readable reason, especially for failures (e.g. "Insufficient balance").
bank_referencerequired for successstringYour 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"}'
GET /transaction

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

ParameterTypeDescription
uuidrequireduuidThe 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"
GET /reconciliation

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

ParameterTypeDescription
statusoptionalstringopen (default), resolved, or all.
pageoptionalintegerPage number, starting at 1.
limitoptionalinteger1–100 cases per page.
🔒
Cases are scoped to the bank that claimed the transaction. Never submit a result without first verifying the bank ledger.

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.

⚠️
No refund execution API exists in the MVP. A refund request on a successful payment starts manual policy review and may be recorded as referred to the authoritative provider or out of scope. Neither disposition means that a refund occurred, and refund execution attempts return 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.

  1. 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.
  2. Create a payment with POST /create_payment Send the amount (seller's amount in SDG). Receive a qr_data string and expiry.
  3. Display the QR / launch the banking app Encode qr_data as a QR code and display it. For inter-app flow, pass it as a deep-link parameter to the banking app.
  4. Poll GET /payment_status or receive a webhook Poll every 2–3 seconds until status changes from requested. Or use webhooks for server push.
  5. Cancel if the QR is abandoned Call POST /cancel_payment when the QR is no longer visible. Never leave a requested transaction 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.

POST /create_payment

Create a new payment intent. Returns a signed QR string and amount breakdown.

Request Body

ParameterTypeDescription
amountrequirednumberSeller amount in SDG. Must be positive. Does not include the SadadSD fee.
purpose_idoptionalintegerID from GET /purposes. Labels the transaction.
operator_idoptionalintegerID from GET /operators. Identifies the POS cashier.
expire_minutesoptionalintegerQR validity in minutes. Range: 1–1440. Default: 15.
reference_internal_idoptionalintegerYour own order / invoice ID for reconciliation.
reference_internal_tableoptionalstringTable 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}'
GET /payment_status

Poll the current status of a transaction. Also auto-expires overdue requested transactions.

Query Parameters

ParameterTypeDescription
uuidrequireduuidTransaction 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"
}
💡
Recommended polling interval: every 2–3 seconds while the QR is displayed. Stop polling when status is success, failed, cancelled, or expired.
POST /cancel_payment

Cancel a pending transaction. Only cancellable while status is requested or scanned. This endpoint is idempotent.

Request Body

ParameterTypeDescription
uuidrequireduuidTransaction UUID.
reasonoptionalstringReason 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"}'
POST /register_webhook

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

ParameterTypeDescription
webhook_urloptionalurlHTTPS URL to receive events. Must resolve to a public IP.
webhook_secretoptionalstring16–128 character secret for signature verification.
GET /transactions

Paginated list of transactions for this endpoint.

Query Parameters

ParameterTypeDefaultDescription
statusoptionalstringallFilter by status: requested | scanned | processing | success | failed | cancelled | expired
pageoptionalinteger1Page number.
limitoptionalinteger20Records per page. Max: 100.

Response

{ "page": 1, "limit": 20, "total": 143, "transactions": [ /* array of transaction objects */ ] }
GET/me

Returns the authenticated endpoint's configuration and its parent merchant's profile.

GET/operators

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.

GET/purposes

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

EventTriggered when
transaction.successBank reports status: "success".
transaction.failedBank 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 2xx response.
  • 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);
🔒
Always use a timing-safe comparison (hash_equals / timingSafeEqual / hmac.compare_digest) to prevent timing attacks.