Skip to content

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.

SignalSourceWhat a move means
Success rate by channelGET /paymentsOne operator degrading, or your own bug
Payments stuck in pendingGET /payments?status=pendingPayers not confirming, or a provider not answering
Available balanceGET /balancePayouts about to start failing
Webhook delivery failuresKonsoleFulfilment 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.

Failure rate over the last day
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' });

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.

Anything pending for more than thirty minutes
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.

FieldWhat it is
totalEverything on the books
availableWithdrawable now
pendingCredited, still inside its retention hold
reserved, hold, disputed, in.transit, creditHeld 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.

Low balance check
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.

Find an event and send it again
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.

ViewWhat it answers
API logsWhat was sent, what came back, how long it took
Routing logWhich provider was chosen, and what fell back
Event streamThe same decision, live, while it happens
Webhook testerDelivery 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.

Carry your own request id through
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.

What did you think of this content?