Salami Gateway processes payment provider notifications (IPNs/callbacks) and forwards them to your application via configurable webhooks. This document covers how provider callbacks are received, how to configure your own webhooks, and the payload formats.
Payment providers send notifications to Salami via this endpoint:
ANY /api/pay/{payment_app}/callback/{action?}
| Parameter | Type | Required | Description |
|---|---|---|---|
payment_app |
integer | Yes | Payment App ID |
action |
string | No | Optional action qualifier (provider-specific) |
This endpoint does not require Bearer token authentication. It is open to receive notifications directly from payment providers.
payment_ipn_requests table.Transaction record.TransactionCompleted event fires, triggering your configured webhooks, Slack notifications, and auto-responses.When registering your callback URL with a payment provider, use:
https://your-domain.com/api/pay/{payment_app_id}/callback
For providers that require action-specific URLs:
https://your-domain.com/api/pay/{payment_app_id}/callback/confirmation
https://your-domain.com/api/pay/{payment_app_id}/callback/validation
https://your-domain.com/api/pay/{payment_app_id}/callback/timeout
https://your-domain.com/api/pay/{payment_app_id}/callback/result
For providers that support transaction validation (e.g., M-Pesa C2B), a separate validation endpoint is available:
ANY /api/pay/{payment_app}/validateTransaction
No Bearer token required. Called directly by the payment provider.
validateTransaction method determines whether to accept or reject the payment.Accept:
{
"ResultCode": 0,
"ResultDesc": "Accepted"
}
Reject:
{
"ResultCode": 1,
"ResultDesc": "Rejected"
}
Webhooks allow Salami to forward transaction events to your application. Configure webhooks through the web dashboard under Payments > Apps > [Your App] > Webhooks.
| Field | Type | Required | Description |
|---|---|---|---|
callback_url |
string | Yes | Your HTTPS endpoint to receive webhook POSTs |
webhook_secret |
string | No | Secret key for verifying webhook signatures |
field |
string | Yes | Transaction field to match against (e.g., status, transaction_type, reference) |
comparator |
string | Yes | Comparison operator (e.g., equals, contains, starts_with) |
keyword |
string | Yes | Value to compare against |
Webhooks support conditional filtering. Only transactions matching your criteria will trigger the webhook.
Examples:
Fire on all completed transactions:
field: status, comparator: equals, keyword: STATUS_COMPLETEDFire only for payments above a threshold (match by narration keyword):
field: narration, comparator: contains, keyword: VIPFire on all incoming payments:
field: transaction_type, comparator: equals, keyword: PAYMENT_IN| Comparator | Description |
|---|---|
equals |
Exact match |
contains |
Field contains the keyword |
starts_with |
Field starts with the keyword |
ends_with |
Field ends with the keyword |
not_equals |
Field does not equal the keyword |
| Field | Description |
|---|---|
status |
Transaction status (STATUS_COMPLETED, STATUS_FAILED, etc.) |
transaction_type |
PAYMENT_IN, PAYMENT_OUT, PAYMENT_NEUTRAL |
payer_account |
Payer's phone/account number |
payee_account |
Payee's phone/account number |
reference |
Payment reference |
narration |
Transaction narration |
amount |
Transaction amount |
currency |
Currency code |
payment_provider |
Driver type identifier |
When a webhook fires, Salami sends a POST request to your callback_url with the transaction data.
| Header | Description |
|---|---|
User-Agent |
Salami-Webhooks/v1.0 |
Content-Type |
application/json |
X-SALAMI-APP-ID |
Payment App ID |
X-SALAMI-ACCOUNT-ID |
Account/User ID of the app owner |
X-SALAMI-SIGNATURE |
HMAC signature of the payload (if webhook secret is configured) |
{
"id": 42,
"payer_account": "254712345678",
"payer_name": "JOHN DOE",
"payee_account": "174379",
"payee_name": "My Business",
"reference": "INV-2026-001",
"transaction_id": "QJI3R7GXWV",
"amount": "100.00",
"currency": "KES",
"transaction_time": "2026-03-15T14:30:00.000000Z",
"narration": "Payment for Order #001",
"status": "STATUS_COMPLETED",
"transaction_type": "PAYMENT_IN",
"transaction_sub_type": "CustomerPayBillOnline",
"payment_provider": "MpesaKeExpress",
"app_id": 1,
"created_at": "2026-03-15T14:30:00.000000Z",
"updated_at": "2026-03-15T14:30:05.000000Z"
}
If you configure a webhook_secret, Salami signs each webhook request. Verify the signature to ensure the request is authentic.
X-SALAMI-SIGNATURE header from the incoming request.$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SALAMI_SIGNATURE'];
$secret = 'your-webhook-secret';
$computed = hash_hmac('sha256', $payload, $secret);
if (hash_equals($computed, $signature)) {
// Signature valid -- process the webhook
} else {
// Signature mismatch -- reject the request
http_response_code(401);
exit;
}
Webhooks that fail (network errors, non-2xx responses) are automatically retried.
| Behavior | Value |
|---|---|
| Retry delay | 5 seconds between attempts |
| Max attempts | Configurable via salami.callbacks.max_attempts |
| Failure handling | After exhausting retries, the webhook is deleted from the queue |
Every webhook dispatch attempt is logged in the webhook_logs table and viewable in the dashboard under Payments > Apps > [Your App] > Webhook Logs. Logs include:
{
"Body": {
"stkCallback": {
"MerchantRequestID": "29115-34620561-1",
"CheckoutRequestID": "ws_CO_191220191020363925",
"ResultCode": 0,
"ResultDesc": "The service request is processed successfully.",
"CallbackMetadata": {
"Item": [
{ "Name": "Amount", "Value": 100.00 },
{ "Name": "MpesaReceiptNumber", "Value": "QJI3R7GXWV" },
{ "Name": "Balance" },
{ "Name": "TransactionDate", "Value": 20260315143000 },
{ "Name": "PhoneNumber", "Value": 254712345678 }
]
}
}
}
}
{
"TransactionType": "Pay Bill",
"TransID": "QJI3R7GXWV",
"TransTime": "20260315143000",
"TransAmount": "100.00",
"BusinessShortCode": "174379",
"BillRefNumber": "INV-2026-001",
"InvoiceNumber": "",
"OrgAccountBalance": "1500.00",
"ThirdPartyTransID": "",
"MSISDN": "254712345678",
"FirstName": "JOHN",
"MiddleName": "",
"LastName": "DOE"
}
{
"Result": {
"ResultType": 0,
"ResultCode": 0,
"ResultDesc": "The service request is processed successfully.",
"OriginatorConversationID": "16740-34861180-1",
"ConversationID": "AG_20191219_00004492b1b6f0af4f53",
"TransactionID": "QJI3R7GXWV",
"ResultParameters": {
"ResultParameter": [
{ "Key": "TransactionAmount", "Value": 500 },
{ "Key": "TransactionReceipt", "Value": "QJI3R7GXWV" },
{ "Key": "ReceiverPartyPublicName", "Value": "254712345678 - JOHN DOE" },
{ "Key": "TransactionCompletedDateTime", "Value": "15.03.2026 14:30:00" },
{ "Key": "B2CUtilityAccountAvailableFunds", "Value": 50000.00 },
{ "Key": "B2CWorkingAccountAvailableFunds", "Value": 100000.00 },
{ "Key": "B2CRecipientIsRegisteredCustomer", "Value": "Y" },
{ "Key": "B2CChargesPaidAccountAvailableFunds", "Value": 0.00 }
]
}
}
}
mc_gross=100.00&protection_eligibility=Eligible&payer_id=BUYER123&tax=0.00&payment_date=14:30:00+Mar+15,+2026+PDT&payment_status=Completed&mc_currency=USD&receiver_email=seller@example.com&payer_email=buyer@example.com&txn_id=ABC123DEF456
{
"topic": "buygoods_transaction_received",
"id": "cac95329-9fa5-42ca-b589-1f3e163c06f0",
"created_at": "2026-03-15T14:30:00.000Z",
"event": {
"type": "Buygoods Transaction",
"resource": {
"id": "cac95329-9fa5-42ca-b589-1f3e163c06f0",
"amount": "100.00",
"currency": "KES",
"till_number": "K000001",
"system": "Lipa Na M-PESA",
"status": "Received",
"sender_phone_number": "+254712345678",
"sender_first_name": "JOHN",
"sender_last_name": "DOE"
}
}
}
In addition to webhooks, you can configure Slack notifications for real-time alerts. Configure Slack webhook URLs under Payments > Apps > [Your App] > Slack Integrations.
Slack notifications are sent for:
Auto-responses automatically send a message (e.g., SMS) to the payer when a transaction is completed. Configure under Payments > Apps > [Your App] > Auto-Responses.