All mutating API endpoints in Salami use an asynchronous request pattern. Instead of blocking until the external API responds, Salami immediately accepts your request and processes it in the background.
1. You send a POST request
2. Salami returns 202 Accepted with a request_id
3. Background worker processes the request (calls Safaricom, QB, etc.)
4. Result delivered via webhook to your callback_url
5. You can also poll GET /api/requests/{request_id} anytime
Include callback_url and webhook_secret in your request body to receive webhook notifications:
POST /api/pay/{app}/requestPayment
{
"phone": "254700000000",
"amount": 100,
"callback_url": "https://your-app.com/webhooks/salami",
"webhook_secret": "your-hmac-secret"
}
{
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "queued",
"poll_url": "https://salami.dgl.co.ke/api/requests/550e8400-e29b-41d4-a716-446655440000"
}
GET /api/requests/{request_id}
Authorization: Bearer {token}
{
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"service": "payments",
"action": "request_payment",
"status": "completed",
"created_at": "2026-06-04T10:00:00+00:00",
"completed_at": "2026-06-04T10:00:03+00:00",
"result": { "transaction_id": "TXN123", "receipt": "ABC..." },
"error": null
}
| Status | Meaning |
|---|---|
queued |
Request accepted, waiting for worker |
processing |
Worker is calling the external API |
completed |
Success — check result field |
failed |
Error — check error field |
All Salami webhooks use a unified envelope format — whether triggered by an async API request, a provider event (payment received, SMS delivered), or a system event (lock accessed, attendance scan).
POST https://your-app.com/webhooks/salami
Content-Type: application/json
X-Salami-Signature: {hmac-sha256-signature}
{
"event": "payments.request_payment",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"service": "payments",
"action": "request_payment",
"status": "completed",
"data": { ... },
"error": null,
"timestamp": "2026-06-05T10:00:03+00:00"
}
| Field | Type | Description |
|---|---|---|
event |
string | Dot-notation event name (e.g. payments.transaction.completed, kra.sale.sync, locks.accessed) |
request_id |
string/null | UUID of the async API request (null for provider-initiated events like payment confirmations) |
service |
string | payments, sms, quickbooks, kra, locks, attendance |
action |
string | What happened (e.g. request_payment, transaction.completed, sale.sync) |
status |
string | completed or failed |
data |
object | Service-specific payload (transaction details, scan data, etc.) |
error |
string/null | Error message if status is failed |
timestamp |
string | ISO 8601 timestamp |
| Event | Trigger |
|---|---|
payments.request_payment |
Async payment request completed |
payments.transaction.completed |
Provider confirmed a payment |
sms.send |
Async SMS send completed |
sms.message.delivered |
SMS delivery report received |
quickbooks.sync_contact |
QB contact sync completed |
kra.sale.sync |
eTIMS sale confirmed by KRA |
locks.accessed |
Lock was unlocked/accessed |
attendance.scans.received |
Biometric scans batch received |
For some services, you may receive two webhooks for a single operation:
request_id present) — your API request was processedrequest_id is null) — the external provider confirmed the resultExample for payments:
POST /api/pay/{app}/requestPayment → 202 Accepted
↓
Webhook 1: { event: "payments.request_payment", request_id: "abc", status: "completed", data: { checkout_request_id: "..." } }
↓ (user enters M-Pesa PIN)
Webhook 2: { event: "payments.transaction.completed", request_id: null, data: { id: 1, amount: 3200, payer_name: "RUTH", ... } }
Use request_id to distinguish: if present, it's the async request result. If null, it's a provider-initiated event.
The X-Salami-Signature header contains an HMAC-SHA256 hash of the JSON payload, signed with your webhook_secret:
$payload = file_get_contents('php://input');
$expected = hash_hmac('sha256', $payload, $yourWebhookSecret);
$received = $request->header('X-Salami-Signature');
if (!hash_equals($expected, $received)) {
abort(401, 'Invalid signature');
}
Webhooks retry up to 6 times with exponential backoff (5s, 10s, 30s, 60s, 120s, 300s) if your endpoint returns a non-2xx response.
/api/pay/)POST /{app}/requestPaymentPOST /{app}/sendMoneyPOST /{app}/sendAirtimePOST /{app}/reverseTransaction/{txn}GET /{app}/checkBalancePOST /{app}/registerUrlsGET /{app}/fetchTransactionsPOST /{app}/simulateTransactionGET /{app}/getTransactionStatus/{txn}GET /{app}/checkPaymentRequestStatus/{txn}/api/sms/)POST /apps/{app}/sendPOST /groups/{group}/sendPOST /apps/{app}/send-telegramPOST /apps/{app}/send-webhook/api/quickbooks/)POST /sync/contactPOST /sync/productPOST /sync/salePOST /sync/purchasePOST /sync/batchGET /sync/logs (read-only, stays synchronous)/api/locks/)POST /{lock}/unlockPOST /{lock}/lockGET /{lock}/statusPOST /syncPOST /{lock}/passcodes (create)DELETE /{lock}/passcodes/{passcode}/api/etims/)Already async — no changes. Uses same pattern with callback_url.
For production deployments, use Redis-backed queues:
QUEUE_CONNECTION=redis
Run dedicated workers:
# External API calls (payments, SMS, QB, locks)
php artisan queue:work redis --queue=external-api --tries=3 --backoff=5,30,60
# Webhook delivery to consumers
php artisan queue:work redis --queue=webhooks --tries=6 --backoff=5,10,30,60,120,300