Monitoring
The signals worth an alert, and where each one comes from.
Most payment incidents are visible in your own data before anyone reports them. Four signals cover almost all of it, and each has one place it actually comes from.
| Signal | Source | What a move means |
|---|---|---|
| Success rate by channel | GET /payments | One operator degrading, or your own bug |
Payments stuck in pending | GET /payments?status=pending | Payers not confirming, or a provider not answering |
| Available balance | GET /balance | Payouts about to start failing |
| Webhook delivery failures | Konsole | Fulfilment silently not happening |
Success rate, per channel
Aggregate on your side rather than asking us for a rate. Pull the window, count by status, and push the number wherever your alerts live.
const today = new Date().toISOString().slice(0, 10);
const page = await wajub.payments.list({ date_from: today, per_page: 100 });
const counts = { succeeded: 0, failed: 0, pending: 0, other: 0 };
for await (const payment of page) {
counts[payment.status in counts ? payment.status : 'other'] += 1;
}
const settled = counts.succeeded + counts.failed;
const failureRate = settled > 0 ? (counts.failed / settled) * 100 : 0;
metrics.gauge('wajub.payments.failure_rate', failureRate, { env: 'production' });Two traps in that snippet
A paged result exposes data, not items, and iterating it with for await walks every page
rather than the first hundred rows. And date_from filters by calendar date, not by timestamp:
there is no fifteen-minute window on this endpoint, so compute short windows from created_at
once the rows are in hand.
Compute the rate against settled payments only. Folding pending into the denominator makes your
failure rate look better exactly when payments are hanging, which is the opposite of useful.
Stuck payments
A payment that is still pending long after creation is either a payer who walked away or a
provider that never answered. The two look identical from outside, which is why the alert is worth
having.
const cutoff = Date.now() - 30 * 60_000;
const stale = [];
for await (const payment of await wajub.payments.list({ status: 'pending', per_page: 100 })) {
if (Date.parse(payment.created_at) < cutoff) stale.push(payment.id);
}
if (stale.length > 0) alert('wajub.payments.stuck', { count: stale.length, ids: stale.slice(0, 20) });Payments expire on their own at expires.in minutes, 24 hours by default, so this alert should
watch the gap between creation and expiry rather than treating every pending payment as a problem.
Balance
GET /balance answers with eight figures, and the two that matter for alerting are available and
pending.
| Field | What it is |
|---|---|
total | Everything on the books |
available | Withdrawable now |
pending | Credited, still inside its retention hold |
reserved, hold, disputed, in.transit, credit | Held back for a specific reason |
Payouts draw on available, so that is the number to alert on. Money sitting in pending is
money you cannot send yet: limits has the hold durations per rail.
const balance = await wajub.balance.retrieve();
metrics.gauge('wajub.balance.available', balance.available, { currency: balance.currency });
if (balance.available < FLOOR_FOR_TOMORROWS_PAYOUTS) {
alert('wajub.balance.low', { available: balance.available, pending: balance.pending });
}The response is cached for three minutes, so polling it faster than that returns the same figures.
Webhook health
Failed deliveries are the one signal you cannot compute from your own database, because a webhook that never arrived leaves no trace on your side. Konsole keeps the delivery history, response codes and retries.
Events are also queryable, which makes recovery possible without a support ticket.
const events = await wajub.events.list({ type: 'payment.succeeded', per_page: 100 });
const event = events.data.find((e) => e.data?.id === paymentId);
if (event) await wajub.events.resend(event.id);Five attempts are made on each delivery, spaced 30 seconds, 1 minute, 5 minutes, 10 minutes and 1 hour. An endpoint that takes longer than 10 seconds to answer is treated as failed and retried, so a slow handler generates duplicates rather than losses.
Konsole
Konsole is where a single failing payment is read end to end, which no metric will ever tell you.
| View | What it answers |
|---|---|
| API logs | What was sent, what came back, how long it took |
| Routing log | Which provider was chosen, and what fell back |
| Event stream | The same decision, live, while it happens |
| Webhook tester | Delivery attempts, response codes, replay |
Correlate your logs with ours
Every response carries X-Request-Id, X-Trace-Id and a W3C traceparent. Send your own
X-Request-Id and it is honoured, up to 64 characters, so one identifier can span your logs and
ours.
curl https://api.wajub.com/payments \
-H "Authorization: sk_test.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…" \
-H "X-Request-Id: checkout-7f3a91" \
-H "Content-Type: application/json" \
-d '{ "amount": 25000, "currency": "XAF", "customer": { "email": "amina@example.com" } }' \
-D -Log that value on your side whenever a call fails. It is the first thing support will ask for, and it turns a vague report into a single row.
Rate limit headroom
X-RateLimit-Remaining is on every response. Graphing it turns a 429 from a surprise into
something you saw coming a week earlier. Rate limits covers the four counters
and the back-off to write.
Platform status
Incidents, degradations and maintenance windows are published on status.wajub.com, with email and webhook subscriptions. Check it before opening a ticket: an operator-side outage is usually already there.