Programmable conditional settlement for modern commerce
Control when money moves. REST API with M-Pesa STK Push, deterministic fund release, and real-time webhook events.
const response = await fetch("https://api.arosapay.com/v1/transactions", {
method: "POST",
headers: {
"Authorization": "Bearer sk_live_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
amount: 25000,
title: "iPhone 14 Pro",
vertical: "electronics",
satisfaction_days: 7,
seller_email: "seller@example.com"
})
});
const transaction = await response.json();
console.log(transaction.payment_link);Five building blocks for conditional payments
Every ArosaPay integration uses these primitives. They compose into any payment flow — from simple checkout to multi-party settlement.
Initiate a conditional payment. Funds are captured and held, not transferred to the merchant.
POST /v1/transactionsDefine rules for fund release: delivery confirmation, inspection period, time-based triggers.
Built into transaction creationBuyer confirms satisfaction, or the system triggers release after the deadline.
POST /v1/transactions/{id}/releaseStructured resolution with evidence, deadlines, and deterministic outcomes.
GET /v1/disputesFunds released to seller after conditions are met. M-Pesa or bank transfer.
GET /v1/payoutsFirst request in 2 minutes
Create a transaction
Send your first POST request to create a conditional payment.
curl -X POST https://api.arosapay.com/v1/transactions \
-H "Authorization: Bearer sk_test_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"merchant_reference": "MERCH-ORDER-100245",
"merchant_order_id": "ORD-100245",
"amount": 25000,
"currency": "KES",
"title": "iPhone 14 Pro",
"vertical": "electronics",
"customer": { "name": "Jane Wanjiku", "phone": "+254712345678", "email": "jane@example.com" },
"seller": { "merchant_id": "merch_12345", "email": "seller@example.com" },
"release_condition": { "type": "buyer_confirmation", "review_window_days": 7 },
"callback_url": "https://merchant.com/webhooks/arosapay",
"idempotency_key": "txn_ORD-100245_20260712"
}'Handle webhook
Listen for transaction.paid and transaction.completed events.
Confirm & release funds
Buyer confirms satisfaction, or the system auto-releases after the deadline.
API surface area
Complete payment infrastructure for marketplaces, platforms, and applications.
API Reference
Complete endpoint documentation with request, response, and error examples
https://api.arosapay.com/v1Reconciliation contract
You send merchant_reference and merchant_order_id. We echo them back in the response and on every webhook. Store the arosapay_transaction_id alongside for support lookups.
Idempotency
Pass a unique idempotency_key per create attempt. Retrying with the same key returns the original transaction with 200 OK — never a duplicate.
Authentication
Authenticate API requests using public and secret key pairs
Your App
Authorization: Bearer sk_...ArosaPay API
Validates & processes
Response
JSON payload
Development environment
sk_test_xxxxxxxxxxxxxxNo real payments processed. Use test phone numbers and card details. Suitable for development and staging.
Production environment
sk_live_xxxxxxxxxxxxxxReal M-Pesa and card payments. Funds are transferred to recipients. Requires completed business verification.
const response = await fetch("https://api.arosapay.com/v1/transactions", {
method: "POST",
headers: {
"Authorization": "Bearer sk_live_your_secret_key",
"Content-Type": "application/json"
},
body: JSON.stringify({ amount: 25000, title: "Order #1234" })
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(error.message);
}
const transaction = await response.json();Authentication Errors
| Status | Code | Description |
|---|---|---|
401 | invalid_api_key | API key is missing, malformed, or has been revoked. |
403 | insufficient_permissions | Key lacks required scope for this operation. |
429 | rate_limit_exceeded | Too many requests. Retry after Retry-After header. |
Key Rotation
Rotate API keys from your dashboard without downtime. When you rotate a key, the previous key remains valid for 24 hours.
API Key Scopes
Integration Examples
Integrate using any HTTP client. No proprietary SDK required.
# Using fetch (built-in, no dependencies required) # Or install node-fetch for Node.js < 18 npm install node-fetch
const API_KEY = "sk_live_your_secret_key";
const BASE = "https://api.arosapay.com/v1";
// Create a transaction
const createTransaction = async () => {
const response = await fetch(`${BASE}/transactions`, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
amount: 25000,
title: "iPhone 14 Pro",
vertical: "electronics",
satisfaction_days: 7,
seller_email: "seller@example.com"
})
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(error.message);
}
return response.json();
};
const transaction = await createTransaction();
console.log(transaction.payment_link);Webhook Events
Real-time lifecycle events, signed with HMAC-SHA256, delivered at-least-once, and deduplicable by event_id.
Event Types
{
"event_id": "evt_hg9v4d3vi4",
"event_type": "transaction.status_changed",
"arosapay_transaction_id": "ap_txn_9x82ksla",
"merchant_reference": "MERCH-ORDER-100245",
"merchant_order_id": "ORD-100245",
"previous_status": "delivered",
"status": "confirmed",
"settlement_status": "approved_for_release",
"amount": 25000,
"currency": "KES",
"occurred_at": "2026-08-30T04:04:56.729Z"
}Signature verification
Compute HMAC-SHA256(secret, timestamp + "." + rawBody) and compare in constant time with X-ArosaPay-Signature. Reject requests where X-ArosaPay-Timestamp is more than 5 minutes off.
const crypto = require("crypto");
// Verify against the RAW request body — do not JSON.parse first.
function verifyArosaPaySignature(rawBody, signature, timestamp, secret) {
const message = `${timestamp}.${rawBody}`;
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(message, "utf8")
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
app.post("/webhooks/arosapay",
express.raw({ type: "application/json" }),
async (req, res) => {
const signature = req.headers["x-arosapay-signature"];
const timestamp = req.headers["x-arosapay-timestamp"];
const eventId = req.headers["x-arosapay-event-id"];
const rawBody = req.body.toString("utf8");
// Reject stale deliveries (>5 minutes) — protects against replay.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.status(400).send("Stale timestamp");
}
if (!verifyArosaPaySignature(rawBody, signature, timestamp, process.env.AROSAPAY_WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
// Deduplicate: at-least-once delivery.
if (await seen(eventId)) return res.status(200).send("ok");
await markSeen(eventId);
const event = JSON.parse(rawBody);
// Reconcile by merchant_reference / merchant_order_id, NOT arosapay_transaction_id alone.
await reconcile(event.merchant_order_id, event.status, event.settlement_status);
res.status(200).send("ok");
}
);At-least-once delivery
Retries run at 60s, 5m, 30m, and 2h. Deduplicate by storing every event_id you have already processed; return 200 on repeats so retries stop.
Reconciliation
Match events by merchant_order_id or merchant_reference — these are the values your system supplied at create time. Store arosapay_transaction_id alongside for support.
Replay protection
Reject any request where X-ArosaPay-Timestamp differs from your clock by more than 5 minutes.
IP allowlist
Optionally restrict incoming webhooks to ArosaPay egress ranges:
154.159.237.0/24, 41.215.96.0/24Lifecycle states (status)
reserved- Funds captured and safeguarded, waiting for delivery
delivered- Seller marked the order as delivered; review window is running
confirmed- Buyer confirmed satisfaction; funds are approved for release
released- Funds released to the seller
disputed- Buyer opened a dispute
under_review- Case is being reviewed by ArosaPay
refunded- Funds were refunded to the buyer
cancelled- Transaction was cancelled before payment
expired- Review window ended without a decision
Settlement statuses (settlement_status)
pending_confirmation- Funds held; waiting on buyer confirmation or delivery
under_review- Frozen pending dispute or manual review
approved_for_release- Cleared for release; payout is being scheduled
released- Paid out to the seller
refund_pending- Refund initiated, not yet completed
refunded- Refund completed
failed- Cancelled, expired, or otherwise not settlable
reversed- Reversal received from the payment rail
What you can build
Marketplaces
Protect buyers and sellers with conditional settlement on every order. Funds release only after buyer confirmation.
COD replacement
Replace cash-on-delivery with digital payment protection. Reduce rejection rates and failed deliveries.
High-value transactions
Electronics, vehicles, real estate — built-in inspection periods with structured dispute resolution.
Logistics-linked payments
Release funds on delivery confirmation from your logistics provider. Integrate via webhook or API.
Infrastructure you can depend on
Availability
Automatic failover and health monitoring across application and API tiers.
Idempotency
Every mutation accepts an idempotency key. Safe to retry without duplicate side effects.
Webhook retries
3 retries with exponential backoff. Failed endpoints flagged automatically with email alerts.
Key rotation
Rotate API keys from the dashboard with a 24-hour grace period. Zero downtime migrations.