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 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.
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.
| 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 |
| Webhooks › Events | Whether the event was recorded, and which endpoints it reached |
| Webhooks › Deliveries | 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.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:
curl https://dashboard.wajub.com/api/statusThe 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.