thekiharani/laravel-payments

Laravel SDK for payment integrations: M-PESA Daraja, SasaPay, and Paystack.

Maintainers

Package info

github.com/thekiharani/laravel-payments

pkg:composer/thekiharani/laravel-payments

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.3 2026-04-28 15:46 UTC

This package is auto-updated.

Last update: 2026-04-28 15:49:45 UTC


README

Laravel package for payment providers:

  • M-PESA Daraja
  • SasaPay v1 merchant APIs
  • SasaPay Wallet as a Service (WAAS) v2 APIs
  • Paystack APIs

The package is a Laravel-native HTTP SDK. It registers container bindings, publishes config, obtains and caches OAuth tokens where providers require them, sends authenticated requests, supports retries and hooks, verifies SasaPay callbacks and Paystack webhooks, and throws typed exceptions for HTTP and network failures.

It does not persist transactions, define your application callback controllers, reconcile settlements, or transform provider callback payloads. Your application owns those concerns.

Requirements

  • PHP 8.2+
  • Laravel 11, 12, or 13

Installation

composer require thekiharani/laravel-payments

The service provider is auto-discovered. Publish the config:

php artisan vendor:publish --tag="payments-config"

Bindings

The package registers:

  • NoriaLabs\Payments\PaymentsManager
  • NoriaLabs\Payments\MpesaClient
  • NoriaLabs\Payments\SasaPayClient
  • NoriaLabs\Payments\SasaPayCallbackVerifier
  • NoriaLabs\Payments\PaystackClient
  • NoriaLabs\Payments\PaystackWebhookVerifier

It also registers the facade alias:

  • Payments

Config

Published config file: config/payments.php

Top-level sections:

  • http
  • mpesa
  • sasapay
  • paystack

Shared HTTP Config

Key Description
timeout_seconds Default request timeout.
default_headers Headers applied to every provider request.
user_agent Optional User-Agent fallback applied when default_headers does not already include one.
cache_store Optional Laravel cache store for provider OAuth tokens. Use true or default for the default store. Leave unset to use only per-client in-memory token caching.
cache_ttl_seconds Optional OAuth-token cache TTL override. When omitted, token expires_in is used.
retry.max_attempts Total attempts including the first request.
retry.retry_methods Methods eligible for retry, for example POST. Empty means all methods.
retry.retry_on_statuses HTTP statuses eligible for retry.
retry.retry_on_network_error Whether connection failures/timeouts are retried.
retry.base_delay_seconds Initial retry delay.
retry.max_delay_seconds Maximum retry delay.
retry.backoff_multiplier Retry delay multiplier.
retry.jitter_seconds Maximum random jitter added to computed backoff delays.
retry.respect_retry_after Whether retryable HTTP responses should honor a Retry-After header before using configured backoff.

M-PESA Config

Key Description
environment sandbox or production.
base_url Optional full base URL override.
consumer_key Daraja consumer key.
consumer_secret Daraja consumer secret.
token_cache_skew_seconds Refresh token before expiry by this many seconds.
b2c_version Default B2C payment API version. Defaults to v1; set MPESA_B2C_VERSION=v3 only when your Daraja app is enabled for the v3 B2C path.
cache_store Optional M-PESA-specific token cache store override.
cache_ttl_seconds Optional M-PESA-specific token cache TTL override.
endpoints Optional endpoint-path overrides keyed by MpesaClient::ENDPOINTS. Useful when Safaricom enables tenant-specific or newer product paths.

SasaPay Config

Key Description
environment sandbox or production.
base_url SasaPay v1 base URL. Sandbox defaults to https://sandbox.sasapay.app/api/v1. Production must be supplied explicitly.
waas_base_url SasaPay WAAS v2 base URL. Sandbox defaults to https://sandbox.sasapay.app/api/v2/waas. Production must be supplied explicitly before using WAAS methods.
client_id SasaPay v1 client ID. Also used for WAAS unless WAAS-specific credentials are configured.
client_secret SasaPay v1 client secret. Also used for WAAS unless WAAS-specific credentials are configured.
waas_client_id Optional WAAS-specific client ID.
waas_client_secret Optional WAAS-specific client secret.
token_cache_skew_seconds v1 token cache skew.
waas_token_cache_skew_seconds WAAS token cache skew.
cache_store Optional SasaPay-specific token cache store override.
cache_ttl_seconds Optional SasaPay-specific token cache TTL override.
endpoints Optional SasaPay v1 endpoint-path overrides keyed by SasaPayClient::ENDPOINTS.
waas_endpoints Optional SasaPay WAAS endpoint-path overrides keyed by SasaPayClient::WAAS_ENDPOINTS.
callback_security.secret_key HMAC secret for inbound callbacks. Defaults to the SasaPay client ID, as documented by SasaPay.
callback_security.trusted_ips SasaPay callback source IP allowlist. Defaults to the documented SasaPay list. Override in published config or with comma-separated SASAPAY_CALLBACK_TRUSTED_IPS.
callback_security.enforce_ip_whitelist Reject callbacks from non-allowlisted IPs when using verifyRequest() or the middleware. Defaults to false; enable it after Laravel trusted proxy handling is configured for your deployment.
callback_security.verify_signature Verify callback HMAC signatures when using verifyRequest() or the middleware. Defaults to true; set SASAPAY_CALLBACK_VERIFY_SIGNATURE=false only if you intentionally rely on a different callback-authentication control.

SasaPay production hosts are not hard-coded. The reviewed SasaPay docs document sandbox hosts clearly, but production hosts are created through SasaPay production applications. Provide production base_url and waas_base_url explicitly.

Paystack Config

Key Description
base_url Paystack API base URL. Defaults to https://api.paystack.co. Paystack uses your API key to determine test vs live mode.
secret_key Paystack secret key used as the bearer token.
endpoints Optional endpoint-path overrides keyed by PaystackClient::ENDPOINTS.
webhook_security.secret_key HMAC secret for inbound webhooks. Defaults to PAYSTACK_SECRET_KEY.
webhook_security.trusted_ips Paystack webhook source IP allowlist. Defaults to the documented Paystack list. Override in published config or with comma-separated PAYSTACK_WEBHOOK_TRUSTED_IPS.
webhook_security.enforce_ip_whitelist Reject webhooks from non-allowlisted IPs when using verifyRequest() or the middleware. Defaults to false; enable it after Laravel trusted proxy handling is configured for your deployment.
webhook_security.verify_signature Verify x-paystack-signature HMAC signatures when using verifyRequest() or the middleware. Defaults to true.

Usage

M-PESA

use NoriaLabs\Payments\MpesaClient;

$mpesa = app(MpesaClient::class);

$timestamp = MpesaClient::buildTimestamp();

$response = $mpesa->stkPush([
    'BusinessShortCode' => '174379',
    'Password' => MpesaClient::buildStkPassword('174379', config('services.mpesa.passkey'), $timestamp),
    'Timestamp' => $timestamp,
    'TransactionType' => 'CustomerPayBillOnline',
    'Amount' => 1,
    'PartyA' => '254700000000',
    'PartyB' => '174379',
    'PhoneNumber' => '254700000000',
    'CallBackURL' => 'https://example.com/mpesa/callback',
    'AccountReference' => 'INV-001',
    'TransactionDesc' => 'Payment',
]);

SasaPay v1 C2B

use NoriaLabs\Payments\SasaPayClient;

$sasapay = app(SasaPayClient::class);

$response = $sasapay->requestPayment([
    'MerchantCode' => '600980',
    'NetworkCode' => '63902',
    'Currency' => 'KES',
    'Amount' => '1.00',
    'PhoneNumber' => '254700000080',
    'AccountReference' => '12345678',
    'TransactionDesc' => 'Request Payment',
    'CallBackURL' => 'https://example.com/sasapay/callback',
]);

SasaPay WAAS Request Payment

use NoriaLabs\Payments\SasaPayClient;

$sasapay = app(SasaPayClient::class);

$response = $sasapay->waasRequestPayment([
    'merchantReference' => 'TOPUP-001',
    'merchantCode' => '600980',
    'networkCode' => '63902',
    'mobileNumber' => '254700000080',
    'receiverAccountNumber' => '600980-1',
    'amount' => '50',
    'transactionFee' => '0',
    'currencyCode' => 'KES',
    'transactionDesc' => 'Wallet topup',
    'callbackUrl' => 'https://example.com/sasapay/waas/callback',
]);

Paystack Initialize Transaction

use NoriaLabs\Payments\PaystackClient;

$paystack = app(PaystackClient::class);

$response = $paystack->initializeTransaction([
    'email' => 'customer@example.com',
    'amount' => 10000,
    'currency' => 'NGN',
    'reference' => 'INV-001',
    'callback_url' => 'https://example.com/paystack/callback',
]);

Paystack Webhook Security

Paystack documents two webhook-origin controls:

  • verify the x-paystack-signature header with HMAC-SHA512 over the raw request body
  • verify the request source IP against the Paystack allowlist

Use the middleware on your webhook route:

use NoriaLabs\Payments\Http\Middleware\VerifyPaystackWebhook;

Route::post('/paystack/webhook', PaystackWebhookController::class)
    ->middleware(VerifyPaystackWebhook::class);

Or verify manually:

use Illuminate\Http\Request;
use NoriaLabs\Payments\PaystackWebhookVerifier;

public function __invoke(Request $request, PaystackWebhookVerifier $verifier)
{
    if (! $verifier->verifyRequest($request, enforceIpWhitelist: true, verifySignature: true)) {
        abort(403);
    }

    // Process the already-authenticated webhook payload.
}

SasaPay Callback Security

SasaPay documents two callback/IPN controls:

  • verify the request source IP against the SasaPay allowlist
  • verify the callback signature with HMAC-SHA512

The signed message format is:

sasapay_transaction_code-merchant_code-account_number-payment_reference-amount

The HMAC secret is the Merchant API Client ID unless you override payments.sasapay.callback_security.secret_key. Signature verification and IP allowlisting are independent controls:

  • SASAPAY_CALLBACK_VERIFY_SIGNATURE=true|false
  • SASAPAY_CALLBACK_ENFORCE_IP_WHITELIST=true|false
  • SASAPAY_CALLBACK_TRUSTED_IPS=203.0.113.10,198.51.100.25

Use the middleware on your callback route:

use NoriaLabs\Payments\Http\Middleware\VerifySasaPayCallback;

Route::post('/sasapay/callback', SasaPayCallbackController::class)
    ->middleware(VerifySasaPayCallback::class);

Or verify manually:

use Illuminate\Http\Request;
use NoriaLabs\Payments\SasaPayCallbackVerifier;

public function __invoke(Request $request, SasaPayCallbackVerifier $verifier)
{
    if (! $verifier->verifyRequest($request, enforceIpWhitelist: true, verifySignature: true)) {
        abort(403);
    }

    // Process the already-authenticated callback payload.
}

The verifier accepts documented SasaPay callback field names only; it does not infer case variants or provider-specific names that are not in SasaPay's published callback examples. The documented signature field is sasapay_signature; if your application receives the signature through another transport, pass it explicitly to verify($payload, signature: $value).

Canonical callback fields include documented aliases across C2B, IPN, checkout/card, B2C, B2B, remittance, utilities, WAAS, and bulk status payloads:

Canonical field Documented aliases
sasapay_transaction_code sasapay_transaction_code, TransactionCode, TransID, SasaPayTransactionCode
sasapay_transaction_id SasaPayTransactionID
third_party_transaction_id ThirdPartyTransID, ThirdPartyTransactionCode, third_party_transaction_code
merchant_code merchant_code, merchantCode, MerchantCode, BusinessShortCode
account_number account_number, accountNumber, AccountNumber, CustomerMobile, MSISDN, RecipientAccountNumber, BeneficiaryAccountNumber, SenderAccountNumber, ContactNumber, DestinationAccountNumber
checkout_request_id CheckoutRequestID, CheckoutRequestId, checkoutRequestId
payment_reference payment_reference, BillRefNumber, InvoiceNumber, MerchantReference, merchantReference, MerchantTransactionReference, TransactionReference, transactionReference, PaymentRequestID, MerchantRequestID, bulk_payment_reference
amount amount, TransactionAmount, TransAmount, AmountPaid, PaidAmount, Amount, RequestedAmount

third_party_transaction_id and sasapay_transaction_id are intentionally not treated as SasaPay transaction-code aliases. Amount formatting is part of the signature input, so keep the exact provider value, for example 1500.00.

Manager Usage

Use the manager when you want custom runtime clients instead of the default container bindings:

use NoriaLabs\Payments\PaymentsManager;

$manager = app(PaymentsManager::class);

$sasapay = $manager->sasapay([
    'environment' => 'production',
    'base_url' => 'https://your-confirmed-production-host/api/v1',
    'waas_base_url' => 'https://your-confirmed-production-host/api/v2/waas',
    'default_headers' => [
        'X-App-Name' => 'billing',
    ],
]);

$paystack = $manager->paystack([
    'secret_key' => config('services.paystack.secret_key'),
]);

Paystack Coverage

Paystack uses one API host for test and live mode: https://api.paystack.co. The secret key determines the environment. The client keeps Paystack field names and amount units exactly as Paystack documents them; pass amounts in provider subunits.

Paystack Auth and Webhooks

API Behavior
PaystackClient::getAccessToken() Returns the configured secret key or custom token-provider value.
PaystackWebhookVerifier::expectedSignature() Computes the HMAC-SHA512 hex digest over the raw request body.
PaystackWebhookVerifier::verify() Validates raw body/signature/IP checks according to configured or per-call toggles.
PaystackWebhookVerifier::verifyRequest() Extracts the raw body, x-paystack-signature, and IP from a Laravel request.
PaystackWebhookVerifier::isTrustedIp() Checks the documented Paystack webhook IP allowlist.
PaystackWebhookVerifier::trustedIps() Returns the active Paystack webhook IP allowlist.
PaystackWebhookVerifier::verifiesSignature() Shows whether signature verification is enabled by default.
VerifyPaystackWebhook middleware Rejects invalid Laravel webhook requests with HTTP 403 according to webhook-security config.

Paystack Transactions, Charge, Bulk Charge, Subaccounts, Splits

Method Endpoint
initializeTransaction() POST /transaction/initialize
chargeAuthorization() POST /transaction/charge_authorization
partialDebit() POST /transaction/partial_debit
verifyTransaction($reference) GET /transaction/verify/{reference}
listTransactions() GET /transaction
fetchTransaction($id) GET /transaction/{id}
transactionTimeline($id) GET /transaction/timeline/{id}
transactionTotals() GET /transaction/totals
exportTransactions() GET /transaction/export
createCharge() POST /charge
submitChargePin() POST /charge/submit_pin
submitChargeOtp() POST /charge/submit_otp
submitChargePhone() POST /charge/submit_phone
submitChargeBirthday() POST /charge/submit_birthday
submitChargeAddress() POST /charge/submit_address
checkPendingCharge($reference) GET /charge/{reference}
initiateBulkCharge() POST /bulkcharge
listBulkChargeBatches() GET /bulkcharge
fetchBulkChargeBatch($code) GET /bulkcharge/{code}
fetchBulkChargeBatchCharges($code) GET /bulkcharge/{code}/charges
pauseBulkChargeBatch($code) GET /bulkcharge/pause/{code}
resumeBulkChargeBatch($code) GET /bulkcharge/resume/{code}
createSubaccount() POST /subaccount
listSubaccounts() GET /subaccount
fetchSubaccount($code) GET /subaccount/{code}
updateSubaccount($code) PUT /subaccount/{code}
createSplit() POST /split
listSplits() GET /split
fetchSplit($id) GET /split/{id}
updateSplit($id) PUT /split/{id}
addSubaccountToSplit($id) POST /split/{id}/subaccount/add
removeSubaccountFromSplit($id) POST /split/{id}/subaccount/remove

Paystack Terminals

Method Endpoint
sendTerminalEvent($id) POST /terminal/{id}/event
fetchTerminalEventStatus($terminalId, $eventId) GET /terminal/{terminal_id}/event/{event_id}
fetchTerminalStatus($terminalId) GET /terminal/{terminal_id}/presence
listTerminals() GET /terminal
fetchTerminal($terminalId) GET /terminal/{terminal_id}
updateTerminal($terminalId) PUT /terminal/{terminal_id}
commissionTerminal() POST /terminal/commission_device
decommissionTerminal() POST /terminal/decommission_device
createVirtualTerminal() POST /virtual_terminal
listVirtualTerminals() GET /virtual_terminal
fetchVirtualTerminal($code) GET /virtual_terminal/{code}
updateVirtualTerminal($code) PUT /virtual_terminal/{code}
deactivateVirtualTerminal($code) PUT /virtual_terminal/{code}/deactivate
assignVirtualTerminalDestination($code) POST /virtual_terminal/{code}/destination/assign
unassignVirtualTerminalDestination($code) POST /virtual_terminal/{code}/destination/unassign
addVirtualTerminalSplitCode($code) PUT /virtual_terminal/{code}/split_code
removeVirtualTerminalSplitCode($code) DELETE /virtual_terminal/{code}/split_code

Paystack Customers, Direct Debit, Dedicated Accounts, Apple Pay

Method Endpoint
createCustomer() POST /customer
listCustomers() GET /customer
fetchCustomer($code) GET /customer/{code}
updateCustomer($code) PUT /customer/{code}
setCustomerRiskAction() POST /customer/set_risk_action
validateCustomer($code) POST /customer/{code}/identification
initializeAuthorization() POST /customer/authorization/initialize
verifyAuthorization($reference) GET /customer/authorization/verify/{reference}
deactivateAuthorization() POST /customer/authorization/deactivate
initializeDirectDebit($id) POST /customer/{id}/initialize-direct-debit
customerDirectDebitActivationCharge($id) PUT /customer/{id}/directdebit-activation-charge
customerDirectDebitMandateAuthorizations($id) GET /customer/{id}/directdebit-mandate-authorizations
triggerDirectDebitActivationCharge() PUT /directdebit/activation-charge
listDirectDebitMandateAuthorizations() GET /directdebit/mandate-authorizations
createDedicatedAccount() POST /dedicated_account
listDedicatedAccounts() GET /dedicated_account
assignDedicatedAccount() POST /dedicated_account/assign
fetchDedicatedAccount($id) GET /dedicated_account/{id}
deactivateDedicatedAccount($id) DELETE /dedicated_account/{id}
requeryDedicatedAccount() GET /dedicated_account/requery
splitDedicatedAccountTransaction() POST /dedicated_account/split
removeSplitFromDedicatedAccount() DELETE /dedicated_account/split
fetchDedicatedAccountProviders() GET /dedicated_account/available_providers
registerApplePayDomain() POST /apple-pay/domain
listApplePayDomains() GET /apple-pay/domain
unregisterApplePayDomain() DELETE /apple-pay/domain

Paystack Plans, Subscriptions, Transfers

Method Endpoint
createPlan() POST /plan
listPlans() GET /plan
fetchPlan($code) GET /plan/{code}
updatePlan($code) PUT /plan/{code}
createSubscription() POST /subscription
listSubscriptions() GET /subscription
fetchSubscription($code) GET /subscription/{code}
disableSubscription() POST /subscription/disable
enableSubscription() POST /subscription/enable
subscriptionManagementLink($code) GET /subscription/{code}/manage/link
sendSubscriptionManagementEmail($code) POST /subscription/{code}/manage/email
createTransferRecipient() POST /transferrecipient
listTransferRecipients() GET /transferrecipient
bulkCreateTransferRecipients() POST /transferrecipient/bulk
fetchTransferRecipient($code) GET /transferrecipient/{code}
updateTransferRecipient($code) PUT /transferrecipient/{code}
deleteTransferRecipient($code) DELETE /transferrecipient/{code}
initiateTransfer() POST /transfer
listTransfers() GET /transfer
finalizeTransfer() POST /transfer/finalize_transfer
initiateBulkTransfer() POST /transfer/bulk
fetchTransfer($code) GET /transfer/{code}
verifyTransfer($reference) GET /transfer/verify/{reference}
exportTransfers() GET /transfer/export
resendTransferOtp() POST /transfer/resend_otp
disableTransferOtp() POST /transfer/disable_otp
finalizeDisableTransferOtp() POST /transfer/disable_otp_finalize
enableTransferOtp() POST /transfer/enable_otp
balance() GET /balance
balanceLedger() GET /balance/ledger

Paystack Payment Requests, Products, Storefronts, Orders, Pages

Method Endpoint
createPaymentRequest() POST /paymentrequest
listPaymentRequests() GET /paymentrequest
fetchPaymentRequest($id) GET /paymentrequest/{id}
updatePaymentRequest($id) PUT /paymentrequest/{id}
verifyPaymentRequest($id) GET /paymentrequest/verify/{id}
notifyPaymentRequest($id) POST /paymentrequest/notify/{id}
paymentRequestTotals() GET /paymentrequest/totals
finalizePaymentRequest($id) POST /paymentrequest/finalize/{id}
archivePaymentRequest($id) POST /paymentrequest/archive/{id}
createProduct() POST /product
listProducts() GET /product
fetchProduct($id) GET /product/{id}
updateProduct($id) PUT /product/{id}
deleteProduct($id) DELETE /product/{id}
createStorefront() POST /storefront
listStorefronts() GET /storefront
fetchStorefront($id) GET /storefront/{id}
updateStorefront($id) PUT /storefront/{id}
deleteStorefront($id) DELETE /storefront/{id}
verifyStorefront($slug) GET /storefront/verify/{slug}
listStorefrontOrders($id) GET /storefront/{id}/order
addStorefrontProducts($id) POST /storefront/{id}/product
listStorefrontProducts($id) GET /storefront/{id}/product
publishStorefront($id) POST /storefront/{id}/publish
duplicateStorefront($id) POST /storefront/{id}/duplicate
createOrder() POST /order
listOrders() GET /order
fetchOrder($id) GET /order/{id}
listProductOrders($id) GET /order/product/{id}
validateOrder($code) GET /order/{code}/validate
createPage() POST /page
listPages() GET /page
fetchPage($id) GET /page/{id}
updatePage($id) PUT /page/{id}
checkSlugAvailability($slug) GET /page/check_slug_availability/{slug}
addProductsToPage($id) POST /page/{id}/product

Paystack Settlements, Integration, Refunds, Disputes, Verification

Method Endpoint
listSettlements() GET /settlement
listSettlementTransactions($id) GET /settlement/{id}/transactions
fetchPaymentSessionTimeout() GET /integration/payment_session_timeout
updatePaymentSessionTimeout() PUT /integration/payment_session_timeout
createRefund() POST /refund
listRefunds() GET /refund
retryRefundWithCustomerDetails($id) POST /refund/retry_with_customer_details/{id}
fetchRefund($id) GET /refund/{id}
listDisputes() GET /dispute
fetchDispute($id) GET /dispute/{id}
updateDispute($id) PUT /dispute/{id}
disputeUploadUrl($id) GET /dispute/{id}/upload_url
exportDisputes() GET /dispute/export
transactionDisputes($id) GET /dispute/transaction/{id}
resolveDispute($id) PUT /dispute/{id}/resolve
addDisputeEvidence($id) POST /dispute/{id}/evidence
listBanks() GET /bank
resolveBankAccount() GET /bank/resolve
validateBankAccount() POST /bank/validate
resolveCardBin($bin) GET /decision/bin/{bin}
listCountries() GET /country
listAddressVerificationStates() GET /address_verification/states

SasaPay Coverage

The SasaPay client intentionally keeps provider field names as documented. It accepts raw arrays and does not translate MerchantCode to merchantCode, CallBackURL to callbackUrl, or similar. Pass the exact payload expected by the specific SasaPay endpoint.

Methods return parsed JSON or text responses. HTTP 4xx/5xx responses throw ApiException. A SasaPay business failure returned with HTTP 200 is returned to you as the provider sent it, because SasaPay uses fields such as status, responseCode, ResponseCode, and statusCode differently across endpoints.

SasaPay Callback Security

API Behavior
SasaPayCallbackVerifier::message() Builds the documented sasapay_transaction_code-merchant_code-account_number-payment_reference-amount message.
SasaPayCallbackVerifier::expectedSignature() Computes the HMAC-SHA512 hex digest.
SasaPayCallbackVerifier::verify() Validates payload/signature/IP checks according to configured or per-call toggles.
SasaPayCallbackVerifier::verifyRequest() Extracts payload, signature, and IP from a Laravel request, then applies the configured or per-call toggles.
SasaPayCallbackVerifier::callbackValue() Reads a canonical callback field from any supported alias, for example third_party_transaction_id.
SasaPayCallbackVerifier::fieldAliases() Returns supported aliases for a canonical callback field.
SasaPayCallbackVerifier::isTrustedIp() Checks the documented SasaPay callback IP allowlist.
SasaPayCallbackVerifier::verifiesSignature() Shows whether signature verification is enabled by default.
VerifySasaPayCallback middleware Rejects invalid Laravel callback requests with HTTP 403 according to callback-security config.

SasaPay v1 Auth

Method Endpoint
getAccessToken() GET /auth/token/?grant_type=client_credentials

SasaPay v1 Payments

Method Endpoint
requestPayment() POST /payments/request-payment/
processPayment() POST /payments/process-payment/
b2cPayment() POST /payments/b2c/
b2bPayment() POST /payments/b2b/
cardPayment() POST /payments/card-payments/
preApprovedPayment() POST /payments/approved/
remittancePayment() POST /remittances/remittance-payments/
businessToBeneficiary() POST /payments/b2c/beneficiary/
registerIpnUrl() POST /payments/register-ipn-url/
lipaFare() POST /payments/lipa-fare/
bulkPayment() POST /payments/bulk-payments/
bulkPaymentStatus() POST /payments/bulk-payments/status/

SasaPay v1 Transactions, Balances, Validation, Utilities

Method Endpoint
accountValidation() POST /accounts/account-validation/
internalFundMovement() POST /transactions/fund-movement/
transactionStatus() POST /transactions/status-query/
merchantBalance($merchantCode) GET /payments/check-balance/?MerchantCode=...
verifyTransaction() POST /transactions/verify/
transactions() GET /transactions/
channelCodes() GET /payments/channel-codes/
utilityPayment() POST /utilities/
utilityBillQuery() POST /utilities/bill-query

SasaPay v1 Dealer Onboarding

Method Endpoint
dealerBusinessTypes() GET /accounts/business-types/
dealerCountries() GET /accounts/countries/
dealerSubCounties($countyId) GET /accounts/sub-counties/?county_id=...
dealerIndustries() GET /accounts/industries/
availableBillNumber() GET /accounts/available-bill-number/
merchantOnboarding() POST /accounts/merchant-onboarding/

SasaPay WAAS Auth

Method Endpoint
getWaasAccessToken() GET /auth/token/?grant_type=client_credentials on the WAAS base URL

SasaPay WAAS Onboarding and Customers

Method Endpoint
waasPersonalOnboarding() POST /personal-onboarding/
waasConfirmPersonalOnboarding() POST /personal-onboarding/confirmation/
waasPersonalKyc() POST /personal-onboarding/kyc/
waasBusinessOnboarding() POST /business-onboarding/
waasConfirmBusinessOnboarding() POST /business-onboarding/confirmation/
waasBusinessKyc() POST /business-onboarding/kyc/
waasCustomers() GET /customers/
waasCustomerDetails() POST /customer-details/
waasUpdateCustomerDetails() POST /customer-details/update/
waasCreateSubWallet() POST /sub-wallets/

SasaPay WAAS Payments

Method Endpoint
waasRequestPayment() POST /payments/request-payment/
waasProcessPayment() POST /payments/process-payment/
waasMerchantTransfer() POST /payments/merchant-transfers/
waasSendMoney() POST /payments/send-money/
waasPayBill() POST /payments/pay-bills/
waasBulkPayment() Alias of v1 bulkPayment() because the current WAAS docs point to /api/v1/payments/bulk-payments/.
waasBulkPaymentStatus() Alias of v1 bulkPaymentStatus() for the same reason.

SasaPay WAAS Transactions, Balances, Lookups, Utilities

Method Endpoint
waasTransactions() GET /transactions/
waasTransactionStatus() POST /transactions/status/
waasVerifyTransaction() POST /transactions/verify/
waasMerchantBalance($merchantCode) GET /merchant-balances/?merchantCode=...
waasChannelCodes() GET /channel-codes/
waasCountries() GET /countries/
waasCountrySubRegions($callingCode) GET /countries/sub-regions/?callingCode=...
waasIndustries() GET /industries/
waasSubIndustries($industryId) GET /sub-industries/?industryId=...
waasBusinessTypes() GET /business-types/
waasProducts() GET /products/
waasNearestAgents($longitude, $latitude) GET /nearest-agent/?Longitude=...&Latitude=...
waasUtilityPayment() POST /utilities/
waasUtilityBillQuery() Alias of v1 utilityBillQuery() because the current WAAS utilities docs point bill query to /api/v1/utilities/bill-query.

The SasaPay docs also contain status-code pages. Those pages document static values, not API endpoints, so they are not represented as HTTP methods.

M-PESA Coverage

Method Endpoint
getAccessToken() GET /oauth/v1/generate?grant_type=client_credentials
stkPush() POST /mpesa/stkpush/v1/processrequest
stkPushQuery() POST /mpesa/stkpushquery/v1/query
registerC2BUrls() POST /mpesa/c2b/{version}/registerurl
c2bSimulate() POST /mpesa/c2b/v1/simulate
b2cPayment() POST /mpesa/b2c/{version}/paymentrequest
b2cPaymentV3() POST /mpesa/b2c/v3/paymentrequest
b2bPayment() POST /mpesa/b2b/v1/paymentrequest
b2cAccountTopUp() POST /mpesa/b2b/v1/paymentrequest with CommandID=BusinessPayToBulk unless already supplied.
businessPayBill() POST /mpesa/b2b/v1/paymentrequest with CommandID=BusinessPayBill unless already supplied.
businessBuyGoods() POST /mpesa/b2b/v1/paymentrequest with CommandID=BusinessBuyGoods unless already supplied.
b2bExpressCheckout() POST /v1/ussdpush/get-msisdn
reversal() POST /mpesa/reversal/v1/request
transactionStatus() POST /mpesa/transactionstatus/v1/query
accountBalance() POST /mpesa/accountbalance/v1/query
generateQrCode() POST /mpesa/qrcode/v1/generate
taxRemittance() POST /mpesa/b2b/v1/remittax
billManagerOptIn() POST /v1/billmanager-invoice/optin
billManagerSingleInvoice() POST /v1/billmanager-invoice/single-invoicing
ratibaStandingOrder() POST /standingorder/v1/createStandingOrderExternal
pullTransactions() POST /pulltransactions/v1/query

M-PESA methods intentionally preserve Daraja field names. The client only string-casts Amount or amount when present and, for the named B2B product helpers, adds the documented CommandID only when the caller has not supplied one.

Endpoint overrides are available when a Daraja tenant is provisioned with a different path:

use NoriaLabs\Payments\Facades\Payments;

$mpesa = Payments::mpesa([
    'endpoints' => [
        'b2c_payment' => '/mpesa/b2c/v3/paymentrequest',
    ],
]);

Helper methods:

  • MpesaClient::buildTimestamp(?DateTimeInterface $dateTime = null): string
  • MpesaClient::buildStkPassword(string $businessShortCode, string $passkey, string $timestamp): string

Request Options

Every provider method accepts either:

  • null
  • array
  • NoriaLabs\Payments\Support\RequestOptions

Fields:

Field Description
headers Request-specific headers.
timeout_seconds Request-specific timeout.
retry Request-specific retry policy or false to disable retries.
access_token Explicit bearer token override.
force_token_refresh Forces the next token lookup to refresh.

Example:

use NoriaLabs\Payments\Support\RequestOptions;
use NoriaLabs\Payments\Support\RetryPolicy;

$response = $sasapay->requestPayment($payload, new RequestOptions(
    headers: ['X-Request-Id' => 'abc-123'],
    timeoutSeconds: 15.0,
    retry: new RetryPolicy(
        maxAttempts: 2,
        retryMethods: ['POST'],
        retryOnStatuses: [500, 502, 503, 504],
        baseDelaySeconds: 0.25,
    ),
));

Custom Token Providers

Implement NoriaLabs\Payments\Contracts\AccessTokenProvider:

use NoriaLabs\Payments\Contracts\AccessTokenProvider;

class MyTokenProvider implements AccessTokenProvider
{
    public function getAccessToken(bool $forceRefresh = false): string
    {
        return 'my-token';
    }
}

Inject via the manager:

$client = app(\NoriaLabs\Payments\PaymentsManager::class)->paystack(
    overrides: [],
    tokenProvider: new MyTokenProvider(),
);

When you supply a custom token provider:

  • the package does not call a provider OAuth token endpoint or use a configured static secret for that client
  • provider credentials become optional for that runtime client
  • you own token freshness

Request Hooks

Use NoriaLabs\Payments\Support\Hooks to observe and mutate transport behavior:

use NoriaLabs\Payments\Support\Hooks;

$hooks = new Hooks(
    beforeRequest: function ($context): void {
        $context->headers['X-Correlation-Id'] = 'corr-123';
    },
    afterResponse: function ($context): void {
        logger()->info('payment response', [
            'url' => $context->url,
            'status' => $context->response->status(),
        ]);
    },
    onError: function ($context): void {
        logger()->error('payment error', [
            'url' => $context->url,
            'error' => $context->error->getMessage(),
        ]);
    },
);

Hook contexts expose:

  • BeforeRequestContext: url, path, method, headers, body, attempt
  • AfterResponseContext: plus response, responseBody
  • ErrorContext: plus error, optional response, optional responseBody

Error Classes

The package throws:

  • NoriaLabs\Payments\Exceptions\ConfigurationException
  • NoriaLabs\Payments\Exceptions\AuthenticationException
  • NoriaLabs\Payments\Exceptions\TimeoutException
  • NoriaLabs\Payments\Exceptions\NetworkException
  • NoriaLabs\Payments\Exceptions\ApiException

ApiException includes:

  • statusCode
  • responseBody
  • details

Async Settlement

For all supported providers, most important operations are asynchronous.

Treat the immediate response as accepted, queued, or processing unless the provider explicitly says otherwise. Final status usually arrives by callback, IPN, transaction-status query, or verification endpoint.

For SasaPay callbacks and Paystack webhooks, verify the provider signature before mutating local order, wallet, or ledger state.

SasaPay Documentation References

The SasaPay endpoint matrix was aligned with the public SasaPay docs:

Paystack Documentation References

The Paystack endpoint matrix and webhook security behavior were aligned with Paystack's public developer docs and official OpenAPI repository: