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 figures refresh every three minutes, so polling 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);

A failed delivery is retried with a growing wait, starting at 30 seconds, for up to 72 hours. 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
Webhooks › EventsWhether the event was recorded, and which endpoints it reached
Webhooks › DeliveriesDelivery 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.xxxxxxxxxxxxxxxx…" \
  -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, and graphing it shows the team's ceiling coming. It counts the team's counter only: a 429 from the per-endpoint counter (30 calls a minute on /payments on the entry plan), from a key's own ceiling or from a named throttle such as payout writes can arrive while the header is still high. Count your 429s by their type (team, api_key, endpoint, payout_writes…) next to the header. Rate limits covers the 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.

The same state is readable by a machine, without a key, so your own monitoring can show it next to your figures:

The platform state, as JSON
curl https://dashboard.wajub.com/api/status

The answer carries status, one of operational, degraded or major_outage, the list of open incidents in active_incidents (id, title, severity, status, is_critical, created_at, started_at), and updated_at. major_outage means at least one open incident is critical.

The API's own health

GET https://api.wajub.com/health answers without a key and reports whether the API and its dependencies are healthy: status is ok, degraded or down, version is the current API version, and a down answers 503. GET /health/ready says whether the API can take traffic. Neither carries latency figures, and neither comes with an availability commitment.

What did you think of this content?