Skip to content

Changelog

What's new on the Wajub platform, by date, version, and product.

All notable changes to the Wajub API and products. The current API version is 2026-10-11 (versioning). Additive changes require no action; breaking changes are released under a new date.

2026-10-12

NEW API

API version 2026-10-12 is the version of new accounts. An existing account keeps its version and reaches this one with X-Wajub-Version; webhooks still follow the current version, 2026-10-11. Its breaking changes:

  • A channel of GET /channels and GET /providers no longer has currency, which read XAF for every channel.
  • currency is required on POST /transfers and on each line of POST /transfers/batch. Earlier versions still default to XAF: send it whatever your version.
  • A payment link has no category_id: POST and PUT /links no longer take it, GET /links no longer filters on it, and a link no longer returns it.
  • A transfer or a refund stopped before it was sent reads cancelled, with the reason merchant_rejected or cancelled_by_wajub, and ?status=failed no longer lists it. Earlier versions read failed. Webhooks follow the current version: such a stop is sent as transfer.failed or refund.failed until 2026-10-12 becomes current, then as transfer.cancelled or refund.cancelled.

Versioning

API Payments

  • POST /payments/{id}/splits charges instalments in live, on Mobile Money. Each call charges the next instalment; the payment is partial until the last one, and a declined instalment keeps the paid ones. Each paid instalment is credited to your balance as it clears, with its share of the Wajub fee, in live as in sandbox; it is paid out to your payout account like any payment, and the Dashboard's payment page shows its hold. Split payments

  • New POST /payments/{id}/close: close a payment paid in part, keeping (outcome: keep) or refunding (outcome: refund) what was collected. It becomes succeeded for that amount, and refunds are measured against it. DELETE /payments/{id} answers 409 payment_partly_paid on such a payment. Close a payment

  • Sandbox no longer splits a Mobile Money charge above the per-transaction ceiling on its own: it answers 422, as live does. Once a payment has an instalment plan, every later charge, the payment page's included, takes the next instalment, and another plan answers 422.

  • New POST /payments/{id}/confirm: finish a charge's OTP, PIN, phone, birthday, address or provider-window step from your server with your secret key. It answers as the charge does: 200, 202 with the next step, or 402 with attempts_remaining. Confirm a step

  • Every announced Mobile Money channel now charges in live: T-Money, Tigo Pesa, HaloPesa, Free Money, Vodafone Cash (Ghana and Egypt), AirtelTigo Money, Zamtel Kwacha, TNM Mpamba, Africell DRC, Moov Money in Gabon, Burkina Faso and Mali, and MTN in Liberia and Guinea. Djamo and Capitec Pay take any number. Phone numbers

  • accept.channels takes mobile for every Mobile Money operator. The payment object returns bearer, expires_at and link, the payment link it came from. Choosing what a payment accepts

  • A card charge takes card_bin at the root of the body, like every field of data. In sandbox, the test card is read at the root as well (card.number, card.holder).

  • A charge on a payment that is already processing answers 409 with error_code: payment_in_progress. The answer

  • A declined charge's 402 carries payer_message, a sentence you can show the payer as it is. Status codes

  • callback must be HTTPS (http://localhost and http://127.0.0.1 accepted in sandbox). metadata.cancel_url is deprecated: the callback is the one return for every outcome. Hosted Checkout

  • GET /payments?reference= lists the payments created with exactly that reference. A payment is read by its id; reference is yours and not unique, and Idempotency-Key prevents duplicates.

  • A failed payment carries failure_reason and failure_message, on the payment and in payment.failed. Failure Reasons

  • A Shield block answers 403 with error_code: shield_blocked and block_reference. Shield blocks

  • An account whose verification is not approved cannot collect any live payment, whatever the provider: 402 merchant_not_verified. Compliance

  • An unknown customer_id on POST /payments answers 422 on that field, not 404 Resource Not Found. operator and save_payment_method are read at the root of a charge. GET /payments/options is documented and answers in the usual envelope.

  • Sandbox charges like live: the failure numbers answer 402 with error_code: insufficient_funds, a local-format number is read in the channel's country, and every announced Mobile Money channel has a test number, Vodafone Cash Egypt (+2010…) and Orange Money Sierra Leone (+23276…) included. Capitec Pay has test numbers of its own (+2761…) that choose the outcome; any operator's test number still pays on it. A number that is not a test number gets a 422 listing the test numbers of the channel's operator. Test scenarios

  • GET /channels: min_amount and max_amount are numbers in the major unit (they were strings a hundred times larger: remove any division by 100), and live and charge say what they promise. A channel's per-transfer ceiling is read in the major unit too. Coverage

  • Every 422 carries status: "Unprocessable Content" and an errors object; a refusal that concerns no single field puts its message under errors.request. Status codes

  • List filters are checked instead of ignored, each with a 422 on its field:

    • every numbered list, tax codes and jurisdictions included: a page that is not a whole number from 1;
    • GET /links and GET /invoices: a per_page outside 1–100, which used to be brought into range, and an unknown sort_by or sort_dir;
    • GET /invoices: an unknown status or payment_status, and a date_from, date_to, due_date_from or due_date_to that is not a date or ends before it starts (it answered 500);
    • GET /links: an unknown type; GET /events: a type no endpoint receives (both answered an empty list);
    • GET /channels: an unknown country; GET /countries: an unknown region; GET /currencies: a type other than fiat, crypto or all, read whatever its case, so type=CRYPTO lists the crypto currencies where it listed the fiat ones.

    GET /links now orders by the requested sort_by and sort_dir first, newest first only among ties. Pagination

  • Rate limits are counted per environment: a sandbox burst no longer uses the live budget. Reads of a transfer no longer count against the payout write bucket. A 429 or a version 400 now carries the CORS headers, X-Wajub-Version is allowed in a preflight, and Retry-After, X-RateLimit-Reset and X-Wajub-Version can be read from a browser. A 429 from a tighter throttle, such as payout writes, answers like the main limiter: type names it (payout_writes), and retry_after and Retry-After give the real wait. X-RateLimit-Reset gives the end of the current window. Rate limits

  • GET / reports in env the environment of the key you call it with. /health and /health/ready return the API version. Monitoring

  • Identity is available on Growth, Scale and Enterprise; Pay as you go answers 403. Identity

  • API reference: every parameter is described with a real example, and the request fields that accept null say so. The invoice, refund, statement and receivable pages show their success answer, and the lists of refunds, beneficiaries, links, accounts and webhook endpoints a complete item. A GET's filters are query parameters only, beneficiary on POST /transfers takes an id or an object, and dispute paths read {id}. The OpenAPI spec declares reusable schemas (Payment, Transfer, Refund, Customer, Beneficiary, Invoice, Link, Account, Event, WebhookEndpoint, Balance, Error) that its responses reference, so a client can be generated from it, and GET /events lists the 77 event types it takes as type. The resource overview pages are available in French. API reference

API Payouts

  • A refused payout answers 422 with an error_code next to message and errors, on POST /transfers, on each refused line of a batch and on POST /accounts/{id}/payouts. A batch line whose beneficiary does not exist says beneficiary_not_found, not "safe to retry". Payout refusals
  • Each batch line gets the checks of POST /transfers: the currency's bounds (amount_out_of_range), and a beneficiary named by its id only (beneficiary_not_found).
  • A payout names its operator's channel (ke.mpesa, eg.vodafone…). The one generic channel is cm.mobile, which works out cm.mtn or cm.orange from the number: in live from Cameroon's real numbering plan, as payments do, in sandbox from the test prefixes. Any other {country}.mobile is refused as an unknown channel. Beneficiaries
  • A deleted beneficiary stays on its transfers and its statement, with deleted: true. Paying a deactivated beneficiary answers 422 beneficiary_inactive instead of 404.
  • A live payout that waits more than 10 seconds answers 429 payout_queue_busy with Retry-After: send it again with the same Idempotency-Key. Limits
  • Payouts in every currency count toward your daily payout cap, payouts held for review included, in the API and in the Dashboard. A sandbox payout whose wallet covers the amount but not the fee is refused with insufficient_balance.
  • An older Dashboard payout still awaiting your confirmation, which reads pending in live, can be confirmed again; it may be held in review before it leaves. After a refusal, the Confirm button can be pressed again. Transfer statuses
  • Documentation: a refused payout keeps the beneficiary you passed inline. The Transfers reference and statuses follow the real response, Balance documents disputed, in.transit and hold, the holds table has a "Wajub restricted your account" row, and the routes sharing the payout write bucket are listed. Transfers, Balance

API Invoices

  • A payment is attached only to an invoice of its own account.
  • GET /invoices: amount_min and amount_max are read in the major unit. The invoice gains customer, the cus_ id; a payment made on an invoice's page names it in invoice. invoice.paid and invoice.payment_succeeded are delivered.
  • POST /invoices/{id}/send answers 422 when the invoice has nobody to send it to and 400 for an invoice already paid, cancelled or refunded. mark-paid refuses a draft, an invoice already paid and an amount of zero. A paid invoice turns refunded once its online payments are refunded in full; an invoice paid in part online does too, with invoice.updated. Invoices
  • Invoices sent through the API take their number from your account's numbering, the sequence the Dashboard uses: your prefix (INV- by default), the year and a four-digit counter that restarts each year (INV-2026-0001). A number is unique within your account.
  • New language field (fr or en) on POST and PUT /invoices and in every invoice response. It sets the language of the e-mails, reminders and PDF, and the invoice page opens in it (add ?locale=fr or ?locale=en to switch). Without it, an invoice takes the linked customer's preferred language, else your account's (Settings → Regional → Language), else en; invoices created in the Dashboard record theirs by the same rule.
  • The invoice PDF is public once the invoice is sent: https://api.wajub.com/i/{id}/pdf, with ?locale=fr|en. invoice.created is sent once, with the lines and totals, and no invoice.updated while the invoice is built. PUT /invoices/{id} sends a single invoice.updated with the lines and totals it saved, and none when it changes nothing.
  • sort_by on GET /invoices now orders the list, and search finds invoices by the name and e-mail written on them. payment_date on mark-paid becomes paid_at whenever the payment settles the invoice. currency on POST and PUT /invoices must be a currency Wajub supports, otherwise 422.
  • On an invoice with no e-mail, the address the payer gives on its page is saved as customer_email, with invoice.updated. An e-mail you set is never replaced.
  • A payment on the invoice page counts the Dashboard's credit notes: the invoice turns paid only once what was paid, minus the credit notes, covers the total. Invoices

Link Payments

  • A payment link's custom_url must be unique across Wajub, deleted links included (422). The link object returns single_use_link and every other field you can set, and viewing a link no longer sends link.updated. Payment Links
  • GET /customers?country= filters on the customer's address country. PUT /customers/{id} keeps name unless a name field is sent with a value. Customers

API Refunds Disputes

  • A live refund that took money from your balance carries ledger_balance_before, ledger_balance_after and ledger_balance_currency. A refund the operator refuses after it was sent stays processing while Wajub completes it another way, never back to pending. A failed refund always carries a failure_reason, from one list in production and sandbox. Refunds
  • refund.succeeded is sent once the refund has left your balance and carries ledger_balance_before and ledger_balance_after, sandbox included. A refund the provider refused and Wajub could not complete another way ends with the provider's reason; ops_failed only when none is known. Handle refunds
  • A refund's reference is only ever yours: the provider's refund id is in provider_reference. Empty refund metadata is {}, and the sandbox refund search matches reference too.
  • dispute.deadline_approaching is delivered two days before a dispute's deadline. dispute.closed follows dispute.lost too. Disputes opened from Pocket get a due_date seven days out, and the same reminder. Disputes
  • Dispute evidence needs a text or a file (422). Evidence and message attachments are private files: every API answer that returns a dispute gives each file a url, a signed link valid 30 minutes, and webhooks carry file_path only. The Dashboard links them the same way and shows a file the payer attaches from Pocket in the dispute's thread. Disputes

Sync

  • A lost dispute returns your commission by the connection's commission_refund_policy, as a refund does, and sandbox moves the commission both ways. fee.received gives the commission in the pricing rule's currency. Account capabilities
  • While a connection's payments are suspended, X-Sync reads work; only calls that move money are refused. A Relay connection carries no invitation date. POST and PUT /accounts accept the merchant's email, a reminder keeps the link you sent valid, and a new link restarts the thirty days, as Resend invitation in the Dashboard now does. dispute.* events are copied to the platform. Onboarding
  • POST and PUT /accounts store your metadata as you send it. Every account returns it ({} when empty); PUT merges it, and a key sent as null is removed. The account shows pricing.min_fee, and GET /accounts?status= filters on the status the account shows, expired included. Subaccounts
  • After onboarding, the merchant comes back to your callback with account, the acc_… id, next to state and status. callback must be HTTPS (http://localhost and http://127.0.0.1 accepted with a sandbox key); a callback already stored keeps working.
  • At most 50 open invitations per environment, after which POST /accounts answers 429; Relay connections do not count. A connection of the other environment answers 404. A Relay connection has no authorization link: authorization_url is null, and /token answers 422.
  • POST /accounts/{id}/payouts returns only the day's automatic payout, which may be failed. Payouts
  • Events the Dashboard sends carry account next to sync_uid. account.updated is sent once, with the whole account, when PUT /accounts/{id} changes an account, when POST /accounts/{id}/token puts a cancelled invitation back to pending, and when Wajub sends a Lite merchant's payout (last_payout_at). A save that changes nothing sends nothing, DELETE sends account.deauthorized, and account.updated can be chosen on an endpoint again. Webhook events
  • Commission receivables: transaction is the payment's id (trx_…), the one GET /payments/{id} reads, in GET /accounts/{id}/commission-receivables and in the account.commission_receivable_recorded and account.commission_receivable_collected webhooks, on every API version.

Tax

  • A refund reverses its share of the tax only when the payment recorded some, and it does so even after Tax is switched off or your plan stops covering it. Reports
  • A payment's tax is recorded at the national rate, as /tax/calculate computes it, and is not recorded while your plan does not cover Tax. Threshold alerts convert sales into the threshold's currency, and the EU threshold counts every member state.
  • The hosted payment page shows no tax line, and converted amounts carry no tax: tax is null in the session. Tax is computed when the payment is credited, never added to what the payer pays.
  • Customer tax ids are checked against the same formats as your own number, with or without separators, and adding a number already on file checks it again (200). gh.vat and gh.tin follow the GRA's format (C0001234567, or a Ghana Card PIN); the old 12-character gh.tin form is refused. Configuration
  • Tax registrations: a duplicate for the same country and state answers 422, a malformed id 404, and PUT can set a revoked registration active again. /tax/calculate answers 422 for an unknown country and 404 for an unknown customer, and removing a customer's tax id with a malformed id answers 404 instead of 500. Tax API
  • Documentation: rounding examples corrected, reverse charge clarified, permissions and pagination documented.

Shield

  • Sandbox payments never affect live risk scoring. Risk scoring
  • Repeated declines on one card BIN at your business now count toward the risk score.
  • The blocklist checks each value against its type and stores it in one form (+237 670 00 00 00 and 237670000000 are one entry). An impossible value answers 422, and removing an unknown entry answers 404. Rules
  • A partial PUT /shield/settings keeps your stored Advanced switches. /shield/stats counts reviews as the queue does and includes disputes under_review. Test rules are still evaluated after the live rule that decides. A dispute accepted in the Dashboard weighs on the payer's reputation as one accepted through the API does. Shield API

Orchestration

  • A route that keeps failing is paused for a few minutes, then tried again; a route that still fails stays paused longer. Waterfall fallback
  • Routing skips a provider Wajub marks as in maintenance (provider_in_maintenance), coming soon or disabled (provider_unavailable); only available and beta providers take payments. The routing simulator names each reason. Routing rules
  • Each attempt records its latency, shown in the payment timeline and per candidate in the routing log. A connection can be limited to chosen currencies, in the setup wizard and on its page. An approved Wajub rail starts at priority 0, like every other connection. Providers
  • Dashboard alerts scoped to a channel or a rule, the rule impact preview and the advisor read the channel and rule each payment was routed by, and "Cascade recovered" counts only failovers. They count payments routed from this release on: earlier payments carry no routed channel or rule.
  • GET /channels returns countries: [] for a global channel instead of [""].
  • Documentation: the timeout to give your client (at least 90 s), client sessions and confirmations never fail over, the providers you can connect, and the international methods still to come on Coverage. Orchestration

Webhooks

  • payment.attempt_failed is sent when an attempt is refused at once and the payment is pending again. payment.expired and payment.cancelled are sent in sandbox as in live; a sandbox expiry no longer sends payment.failed.
  • Every delivery attempt is signed with the endpoint's current secret, so a rotation covers the retries too.
  • link_payment_complete is no longer delivered (payment.succeeded covers it), and GET /events lists only the types that are delivered.
  • In payloads, an empty object such as metadata stays {} instead of [], when delivered, resent or listed. The payment.split.* events carry the full payment, payment_method included.
  • beneficiary.created is also sent for payout accounts added in the Dashboard, and transfer.processing only once when a held payout is approved. Webhook events

Components CLI Dashboard

  • Wajub.js 1.6.0: onSuccess, onBreakdown and fetchSession() give amounts in major units (12.5 for 12.50 EUR); remove any division by 100. onSuccess carries id, the payment's Wajub id. Back from the hosted checkout, reference is that id and trxref your reference. Inline, Done and close() call onClose. CheckoutState gains four step states, every wrapper re-exports the core types, and WajubPay.create() is deprecated. API reference
  • Wajub.js 1.6.0: a hosted checkout that cannot load its session closes the overlay or removes the inline frame right after onLoadError, without onClose. submit() with no pay button to press calls onError with nothing_to_submit. Each checkout listens only to its own iframe, so with two on a page each one receives only its own close and success. In Vue, <CheckoutEmbed> shows its loading state when show-loading is absent.
  • @wajub/svelte 1.6.0 ships its components as source, compiled by your project's own Svelte 4 or 5, and they now carry prop types. A Svelte 5 project that forces runes on every file must leave node_modules out, as npx sv create does. Svelte
  • Hosted checkout: a payer who tries again while the previous attempt is still processing waits for its outcome on a "payment in progress" screen, in their language, instead of an English error. If that attempt ends without charging, the checkout goes back to the payment method and shows the refusal's payer_message. The return after a failed payment carries reference (the Wajub id) and trxref (your reference). Hosted checkout
  • CLI 1.7.3 completes payment.attempt_failed and dispute.deadline_approaching, and triggers every Mobile Money channel with its sandbox number. A login no longer moves your default profile: a first sandbox login becomes the default, a live one never does (wajub config use <profile>). With -d, a value with a leading zero or more than 15 digits, and a field named phone, ending in _phone, account_number or reference, stays text. wajub doctor requires Node.js 22, and wajub open balance opens the balances page. wajub listen --events no longer completes payment.checkout_page_opened, payment.redirected_to_callback or link.expired, which no endpoint receives. Triggers
  • Dashboard: a team can hold a second public key and edit or delete one. The Developer role manages providers, domains, the sandbox/live switch and orchestration (routing rules, channel policies, alerts), and can run the routing simulator and preview a rule's impact. Members who manage settings can require two-factor authentication. A member behind a blocked IP can still change their own password, two-factor and passkeys. The connection wizard leaves opt-in channels unticked. Team and security
  • Dashboard: signing in with a passkey counts as the second factor. Close one of your other sessions from Settings, Security. Filter the team audit log by member. A disabled API key is marked Disabled, and restricted keys do not offer a settings scope. Approving wajub login or another device pairing requires the permission to manage API keys.
  • Dashboard: the sites allowed to embed your checkout have their own page, Settings → Checkout embedding, on every plan including Pay as you go, and the Developer role can change them. Once the list is set, only those sites can show your checkout. While it is empty, the Dashboard reminds you to declare them: do so before you take live payments.
  • Dashboard: changing Team security, creating or editing an API key, creating, editing, deleting or promoting a webhook endpoint, inviting, changing or removing a member, leaving a team, changing the payout security settings and saving the embedding list ask you to confirm your identity; saving the embedding list is also recorded in the audit log. A webhook endpoint created without a description gets a validation error instead of a server error.
  • Konsole: the API logs record each request's API version. Log explorer

SDKs Plugins

  • Server SDKs retry the 409 the API sends while a request with the same Idempotency-Key is still running, and return the first request's result: Node.js and C# 2.0.0, PHP 2.0.1, Ruby 1.3.0, Python, Go and Java 1.2.1. Go keeps one Idempotency-Key across its retries. Idempotency
  • Server SDKs, same versions: security fixes in webhook signature verification. Update your server SDK.
  • Node.js SDK 2.0.0: results are typed objects where 1.5.0 returned plain records, WebhookEvent carries the type in event, and expires.in is typed. payments.initialize is gone: call create. Node.js SDK
  • C# SDK 2.0.0: Webhooks.ConstructEvent returns plain values (string, long or double, bool, dictionaries and lists) instead of JsonElement. Code that read JsonElement must switch to the plain values. C# SDK
  • Ruby SDK 1.3.0: the tax methods (reports, codes, registrations, jurisdictions, thresholds) and the Shield blocklist methods are in the gem, as in the other server SDKs. Ruby SDK
  • React Native 1.4.1, Android 1.2.1 and Flutter 1.5.0: 401, 404 and an ended session (410) are not retryable, and a server error (5xx) is an API error. React Native and Android cancel the payment when the payer closes the sheet before submitting, as Flutter does, whose showWajubPaymentSheet now calls onCancel. The React Native and Flutter sheets speak French. React Native opens the 3D Secure page on Android 11+ without a <queries> block, and @stripe/stripe-react-native is truly optional. Flutter's watchStatus() follows the payment's Wajub id and keeps polling for the whole watch. Mobile SDKs
  • WordPress plugin 1.2.1: security fix for paid content and for LearnDash, LifterLMS and Tutor LMS course enrolment. Update to 1.2.1. The course buttons show only to signed-in visitors. Every integration reads the payer's return by the Wajub payment id, and an order made before 1.2.1 finds its payment by the reference the plugin sent. WordPress
  • WordPress plugin 1.2.1: a partial refund started from WooCommerce is recorded once. MemberPress sends the transaction receipt. The legacy webhook address answers 403 to a delivery it refuses.
  • Documentation: a mobile callback must be an https App Link or Universal Link. The Components pages document isOpen(), field heights, the field types and properties, with examples that compile.

2026-10-11

NEW API

New dated API version: 2026-10-11, with one breaking change: a payment's status reads refunded or partially_refunded once refunds have succeeded, and GET /payments?status= filters on that reading, so a refunded payment is no longer listed under succeeded. Send X-Wajub-Version: 2026-09-01 to keep reading succeeded. What the payment's status says

Your account does not move. Every account that existed is now pinned to 2026-09-01, and a new one is pinned to the version current at its creation. Until now an account carried no version and followed the platform's, so a new version would have reached it the day it shipped. Webhook payloads are still shaped for the latest version, whatever your account's. Versioning

API Sync Shield Konsole

  • A dispute you accept or close weighs less on the payer's reputation than one ruled against you. The dispute now carries metadata.resolution set to accepted. Risk scoring
  • POST /beneficiaries/{id}/verify now asks the operator for every account. Outside Sync Relay, checks made before this release did not reach the operator: check again any beneficiary whose name you rely on. On your own beneficiaries the answer is information: the call answers 200 with verification (verified, name_mismatch or lookup_unavailable) and registered_name, the name the operator holds, and the beneficiary stays payable whatever it says. A sandbox key always gets lookup_unavailable: sandbox has no operator. On a Relay account the call is still a gate and returns no name. A payout account added in the Dashboard can no longer be verified through this call. Checking the name on a wallet
  • Documentation: an Idempotency-Key on POST /payments, /transfers, /transfers/batch and /refunds never expires; only the stored response lasts 24 hours. A key reused for something new answers 201 with the old resource and does nothing. Idempotency
  • Coverage now marks as coming soon the rails no provider you can connect carries yet: Egypt, Algeria, Morocco, Tunisia, Sierra Leone, OPay and PalmPay in Nigeria, and crypto. Coverage
  • Mobile SDKs: Flutter and React Native 1.4.0, Android 1.2.0. Cards through a provider that collects them on its own page (Paystack, Flutterwave, PayPal, Kkiapay, FedaPay, CinetPay) and one-tap wallets such as Djamo. A Mobile Money payment comes back as an action to approve, push_approval. Mobile SDKs
  • Shield: Always allow customer on a review is open to every plan. An attempt Shield blocked is in your review queue, and allowing the customer lets their next payment through, unless they are on your blocklist or match a sanctions list. Shield, lists shows the customers you allowed and removes one. Blocking or refunding from a review, custom rules and the blocklist stay with Shield Advanced. Rules
  • A recurring invoice now produces its copies: the next one is due one interval after invoice_date, never in the past. Each copy is an ordinary draft, generated once per cycle, that keeps the whole-invoice discount; it still has to be sent and paid. The Dashboard's start and end dates are now applied. An invoice flagged recurring before this stays inert until its recurrence is saved again. Recurring invoices
  • Konsole: disabling or enabling a webhook endpoint from its edit form now takes effect.
  • Konsole: a new endpoint's signing secret is shown once when you create it, Reveal works on the endpoint page, and Promote to Live creates the live endpoint and shows its secret once.
  • Konsole: searching the API logs finds a call by its X-Request-Id, in live as in sandbox. The JSON export carries the full records, and the exports of logs and events now apply the filters on screen.
  • Konsole: new filters for the endpoint (API logs), the category and delivery status (events) and the winning provider (routing log).
  • Konsole: resending a delivery whose endpoint already accepted the event is refused and does not count against your Webhook replays per month. With no row ticked, bulk retry takes the failed deliveries of the last hour, 24 hours or 7 days. Webhook tester
  • Konsole overview: Diagnose is offered on the authentication, latency and endpoint cards. "Unused API key" watches your API keys, and "Signing secret older than 6 months" counts from the last rotation.
  • API logs redact fewer harmless fields and show a longer key prefix. Log explorer
  • POST /beneficiaries/{id}/verify has a ceiling of its own on every plan: 10 checks a minute and 200 a day per account in live, 10 a minute in sandbox. Past it the answer is 429. Limits
  • Konsole no longer offers payment.checkout_page_opened and payment.redirected_to_callback for a webhook endpoint: they are never delivered. An endpoint that lists one keeps it, without effect. Webhook events
  • You can stop a recurring invoice whatever its status, paid included: POST /invoices/{id}/stop-recurring, or Stop recurring in the invoice's actions menu. The copies already generated stay. The invoice object now carries next_invoice_date, when the next copy is due, and recurring_parent, the recurring invoice a copy was generated from. Stopping a recurrence
  • refund.updated is now delivered, and an endpoint can subscribe to it. Who sends a live refund
  • A refund in a three-decimal currency such as TND keeps every decimal, so a full refund or an accepted dispute goes through. In a currency with no subunit, such as XAF, a refund's fee share and payer_amount are whole numbers. Who bears the fee
  • With X-Sync, a platform sees only the customers its connection created or used. A payment is created without any other customer, and their cus_ id answers as unknown. DELETE /customers/{id} answers 403: erasing a customer is the merchant's decision. Account types
  • DELETE /customers/{id} now also removes the customer's details from invoices, a payment's addresses, the bodies of webhook deliveries (and their retries), the copies a Sync platform received, analytics and search. A repeated call finishes an erasure an earlier one left incomplete. Deleting a payer
  • POST /events/{id}/resend delivers again to endpoints that have not accepted the event.
  • With X-Sync, an Idempotency-Key belongs to the connection, and accepting or closing a dispute is refused once the connection's payments are suspended.
  • A custom rule on ip_country reads only a verified payer address. If you run your own checkout
  • Retrying a payout batch with the same Idempotency-Key returns the lines already sent, even when the hour has no place left. Limits
  • POST /beneficiaries/{id}/verify makes a beneficiary left unverified payable when the names match. Checking the name on a wallet
  • A payment sent to review because the payer's name is close to a sanctions listing appears in Shield, Reviews even when Shield is switched off. Risk scoring
  • A generated recurring copy stays a draft until you send it, past its due date too; a copy that read overdue without being sent is a draft again. A series turned off and on resumes after its latest copy, cancelling clears next_invoice_date, and a quote, an estimate or a generated copy cannot be made recurring (422). Duplicating a recurring invoice gives a plain draft. Recurring invoices
  • Konsole: Rotate on an endpoint's page replaces its signing secret, after your password, and shows the new one once. Webhook tester
  • Webhook payloads are always shaped for the current API version, whatever the version of the call that raised them.

API Payments Invoices Sync Dashboard

  • POST /payments/{id} now returns what the payer's next step needs: a 202 that names a step in action carries action_url, otp_id and otp_masked_contact, the pin, phone, birthday and address identifiers, and client_session. crypto.deposit is a sandbox field. When action is set
  • Wave (ci.wave, sn.wave), Express Union (cm.eu) and M-Pesa (ke.mpesa, tz.mpesa, mz.mpesa) can be charged in live. Wave and Express Union take any number. M-Pesa takes a Safaricom number in Kenya and a Vodacom one in Tanzania and Mozambique, and mobile or a country aggregate sends such a number to M-Pesa. Every other channel takes only a number of its own operator. Letting Wajub find the operator
  • A payment created with accept can be charged on mobile or on a country aggregate such as cm.mobile. The restriction is checked on the operator the number resolves to, and a number that resolves outside what the payment accepts is refused. Choosing what a payment accepts
  • A numbered page of a list now carries next_cursor and has_more in meta, next to the page counts. Send that next_cursor as cursor and the walk continues right after the last item of the page. On every list that takes a cursor; GET /invoices and GET /links sorted with sort_by stay page-numbered. With the PHP SDK, getNextPage() and autoPagingIterator() follow that cursor from the second page on: same items, but the meta of those pages no longer has current_page, last_page and total. Pagination
  • A file attached to a dispute message with POST /disputes/{uid}/messages is now stored. Send message
  • An invoice with no customer_email and no customer_id can now be paid: its page asks the payer for an e-mail, used for the payment and its confirmation. On that page, the e-mail you put on the invoice is always kept. Invoice quickstart
  • An online payment on an invoice adds to what you recorded with mark-paid. Recording a payment you collected elsewhere
  • Sync: an accepted subaccount never reads expired. expires_at is the invitation's date and is cleared when the merchant accepts, including on connections accepted before, and payments on an accepted connection go through. The Dashboard asks the merchant again before a pricing change. Onboarding
  • Sync: every account.* event the API sends names its connection in data.account, the acc_… id. account.deauthorized, account.payment_activated and account.payment_suspended gain the key; data.id is not always the connection. Webhook events
  • API keys, Dashboard: a key's expiry can be set when it is created, changed or removed on a private or restricted key, and is shown next to the key. Editing anything else keeps it. The IP allow-list accepts IPv4 and IPv6 addresses and CIDR ranges in both families, on creation and on edit, and an IPv6 entry matches however it is written. Environments and keys
  • Orchestration: the provider connection wizard pre-fills priority 0, the documented default, instead of 50. Existing connections keep their priority. Providers
  • Docs playground: Test this endpoint works once you are signed in, in sandbox only. API reference
  • Documentation: callback on a Sync connection is the page the merchant comes back to, not where events are sent. Events go to your webhook endpoints, and account.connected needs a subscription by name. Account capabilities
  • Documentation: the reference of POST /beneficiaries requires channel and name and has no type; POST /transfers describes the inline beneficiary (name and channel required, no bank_code) and says reference is never checked for uniqueness, only Idempotency-Key protects a retry; a transfer's complete is true only on succeeded; the Event object lists event, created, livemode, pending_webhooks, api_version and request. Transfers, Events
  • Documentation: the Tax quickstart now sets inclusive: true, because it charges the calculated total. With inclusive: false, a payment of 11 925 XAF recorded 2 296 XAF of tax instead of 1 925. If you followed the earlier version, set inclusive to true; past records are not recomputed. Tax quickstart
  • Documentation: the advice to keep five to ten calls in flight is replaced by a rate per minute under each resource's ceiling for your plan (24 calls a minute on /payments on Pay as you go, where the ceiling is 30), with an example that waits Retry-After. Performance
  • Documentation: in the Payment Links quickstart, extra fields are described with name and type, not key. The first example of the Components home page sends email; without a payer contact the API answers 422. The Django marketplace cookbook creates the connection with withdrawals, stores the payout_phone its payout example uses, and reads the connection in data.account. Payment Links quickstart, Django marketplace

Components SDKs CLI Plugins

  • Wajub.js 1.6.0: locale given to mount(), open(), checkout() or components() is now the language the checkout or field opens in. Hosted checkout
  • Wajub.js 1.6.0: a hosted checkout whose session cannot load calls onLoadError with session_not_found or session_terminal_expired. It called onError: move that handling to onLoadError. API reference
  • Wajub.js 1.6.0: checkout.submit() presses the pay button of the method the payer is on. unmount() removes the hosted checkout and keeps the instance, mount() puts it back, inline and overlay. API reference
  • @wajub/vue and @wajub/svelte 1.6.0: useWajubOptional() returns a ref (Vue) or a store (Svelte) that follows the runtime. Read runtime.value or $runtime. @wajub/svelte works from the first render, with Svelte 4 and Svelte 5. Vue, Svelte
  • PHP SDK 2.0.1: create(), retrieve() and update() on transfers, beneficiaries, links, invoices, accounts and webhookEndpoints no longer throw Cannot instantiate abstract class after the request was sent. If a transfers->create() failed this way on 2.0.0, the transfer was created: check before sending it again. PHP SDK
  • Python SDK 1.2.1: the package on PyPI contains the module again. 1.1.0 and 1.1.1 installed an empty wheel. Install with pip install "wajub>=1.2.1". Python SDK
  • Ruby SDK 1.3.0: customers.delete_tax_id calls DELETE /customers/{id}/tax_ids/{tax_id}. Ruby SDK
  • payments.initialize, an alias of create, is no longer in the Node.js and C# SDKs (2.0.0), nor in Python, Go and Java (1.2.1) and Ruby (1.3.0). PHP keeps it. Call create. SDKs
  • SDK examples on the payout, balance, beneficiary, payment link, invoice and dispute pages now match each SDK's real signatures. Dispute evidence with a file is shown as a multipart request.
  • React Native SDK 1.4.1: watchStatus() reports when realtime is on, and polling runs for the whole watch. Android SDK 1.2.1: watchStatus() opens the realtime socket and polls next to it. Mobile SDKs
  • CLI 1.7.2: wajub trigger --channel pays with a sandbox number of that channel's operator, --channel ci.orange included. Triggers
  • WordPress plugin 1.2.1: a customer back from the payment page finds their order in the state of its payment (WooCommerce, GiveWP); refunds from WordPress are accepted; a Dashboard refund reaches WooCommerce; [wajub_pay_content] unlocks. WordPress

2026-10-10

API Orchestration Sync Tax Shield Webhooks Components CLI

  • Sync and Tax calls need the product in your plan or add-on: without it, /accounts, any call carrying X-Sync and /tax answer 403 with error_code: feature_not_in_plan. Sync · Tax
  • A live webhook endpoint must use https://: creating one on http://, or pointing one at an http:// URL, answers 422. Sandbox endpoints still accept http://. Live endpoints already on http:// keep their deliveries, and Konsole flags them. Webhooks
  • customer_id on POST and PUT /invoices takes the customer's cus_ id. Invoices
  • PUT /accounts/{id} answers 409 with error_code: merchant_not_verified when it sets payment_status: active on a merchant whose verification is not approved. Onboarding
  • With X-Sync, a platform reaches only what the connection brought. Disputes are those of its payments; links, invoices and beneficiaries are those it created, and a payout goes only to one of those beneficiaries; GET /events lists the events about all of these, and POST /events/{id}/resend sends to your own endpoints. Anything else answers 404. /balance, /webhooks, /shield, /tax and /listen answer 403 with X-Sync. A payment on a connection's link or invoice carries the connection, and is refused once the connection may no longer collect. write, settings, subscriptions and analytics are no longer capabilities: asking for one answers 422. Account types
  • DELETE /customers/{id} anonymises and deletes the customer; if a part fails, it answers 500 and changes nothing. customer.deleted carries the anonymised record and is no longer followed by customer.updated. The customer's details are removed from the events already recorded, a repeated call answers 200 instead of 404, and the archived copy is deleted after 120 days. Deleting a payer
  • The hourly payout velocity counts accepted payouts only: a refused one, for want of funds or by the ceiling itself, takes none of the six places, so retrying it does not extend the wait. A batch takes one place whatever its number of lines; each line still adds its amount to the hourly amount. Limits
  • On a charge made with a secret key, have the payer's IP scored: call Wajub.signals() (Wajub.js 1.6.0) in the payer's browser and pass the result as data._client. Its token vouches for the payer's address. If you run your own checkout
  • A payer whose name is close to a sanctions listing, without being it, is now sent to review whatever the score. You can choose to refuse it instead, with sanctions_near_match_action on PUT /shield/settings or in Shield, Risk controls. An exact match is still always refused. Risk scoring
  • A payment now moves on to your next provider when the first one answers that the path is down: the operator timed out or is unavailable, your credentials are refused, or its float is exhausted. A refusal about the payer still stops there. What stops the ladder
  • A live refund says who sends it: execution_mode (automatic, merchant or manual) and, when it is not sent automatically, manual_reason with a stable code and a message. refund.updated is sent when either changes. A refund that is still processing counts against what is left to refund. Who sends a live refund
  • You choose who bears a payment's Wajub fee when you refund it: fees_bearer on POST /refunds and in the Dashboard's refund form, with a default in Settings, Refunds. merchant gives the customer back everything they paid for the refunded share, customer leaves the fee share with them. The refund carries fee_amount and payer_amount, and your balance is debited payer_amount. Nothing changes unless you set it: the fee stays with whoever paid it. Who bears the fee
  • A payment created with bearer set to customer costs you no fee: the fee is taken once, from the payer, and no fee.charged is sent for that payment. Sandbox does the same. Who pays it
  • Accepting or closing a dispute through the API now refunds the payer: POST /disputes/{id}/accept and /close create a refund of the disputed amount with the reason customer_complaint. That refund does not debit you a second time. A chargeback, which the payer's bank has already paid back, is not refunded on top, from the API or the Dashboard. dispute_settlement is a reserved key in a refund's metadata. Conceding
  • Wajub Components: fetchSession() works from any merchant site. Sessions
  • js.wajub.com now sends Access-Control-Allow-Origin: *, so importing wajub.mjs as a module works. Wajub.js
  • CLI 1.7.1: wajub logout also removes the key from config.json.bak. Delete any config.json.bak an older version left behind. Log out

2026-10-09

NEW Payments Components

  • deferred: true on POST /payments prepares a payment for payment fields shown before the order exists: no transaction, no payment.created and no line in your payments until POST /payments/{id}/activate creates it with the final total and the customer. Before the order exists
  • PATCH /payments/{id} changes a prepared payment's amount and currency while the checkout is open, without remounting the fields. Update a prepared payment
  • Wajub Components: variant: 'fields' fits a component's inputs into your own payment method box, and fields.country and fields.operator: 'detect' drop the country and operator lists from mobile money. Also onBeforeSubmit, fields.customer: 'external', and the bankTransfer and crypto components. The runtime served from js.wajub.com reads them, @wajub/js 1.5.0 adds their types, and @wajub/vue and @wajub/svelte 1.5.1 pass variant. Inside your own payment method box

Orchestration

  • The router applies the most specific instruction first: the connections your matching routing rules name, each followed by the rule's fallback, then the channel's policy, or the connection priority on a channel without one. Orchestration
  • Routing rules now apply their weight and their advanced conditions: time windows, customer segments, metadata filters and frequency caps. A deleted rule never routes. A metadata filter in takes values separated by commas, and eq and in compare by text, so 100 typed in the Dashboard matches a numeric value. Routing rules
  • A connection's fixed fee is converted from your base currency into the payment's currency before the router compares costs.
  • A channel policy's cascade order, objective and guardrails order the connections no rule placed. A guardrail moves a connection to the back, it never refuses a payment.
  • The routing simulator on the Routing Rules page runs the real router in a dry run.
  • A card entered in a provider's own fields is only charged by that provider, and never failed over. A Stripe decline is final and carries Stripe's decline code.
  • A connection without credentials is skipped: the next one is tried. A missing or rejected key is flagged on the connection, and your team is emailed about it. Waterfall & fallback

2026-10-03

CLI

  • CLI 1.7.0: wajub completion replaces wajub autocomplete. Run wajub completion install once; wajub autocomplete remains as an alias, and a shell set up by it keeps completing after the upgrade. Completion now covers values (statuses, event types, API paths, profiles), fish and the bash macOS ships. CLI
  • CLI 1.6.1: the first published release with listener sessions (1.6.0 never reached npm).

2026-10-02

CLI

  • CLI 1.6.0: wajub listen streams through listener sessions, and your handler receives each delivery exactly as production sends it. CLI 1.5.0 and earlier can no longer listen: their wajub listen stops and asks you to update (npm install -g @wajub/cli@latest). Every other command of older versions keeps working. Listen
  • CLI 1.5.0: the Agent Skills are copied into ~/.agents/skills and ~/.claude/skills on the first command after installing or upgrading, without wajub login. Opt out with WAJUB_NO_SKILLS=1.

2026-09-19

API Payments

  • The rate limit is now your plan's ceiling per minute, shared by all your keys, with a smaller share for payments, payouts and a few other routes, and no ceiling on Enterprise except payout writes (creating beneficiaries, transfers and batches), capped at 20 a minute on every plan. Rate limits
  • A chargeback opened at a provider now reaches you as a dispute, with its deadline in due_date.

2026-09-17

NEW Payments Transfers Sync

  • In live, raw card data sent to POST /payments/{id} is refused.
  • Receivables: POST /beneficiaries/{id}/receivables records what a paid-out seller owes back, and a batch payout line can take it back first. New events beneficiary.receivable_recorded and beneficiary.receivable_recovered.
  • Payouts to a Wajub account.
  • Sync: account.change_requested when PUT /accounts/{id} proposes new terms, and account.commission_receivable_recorded and account.commission_receivable_collected for a commission that could not be taken at the source.

2026-09-16

NEW Transfers Sync

  • POST /transfers/batch: up to 500 payouts in one call, with a required Idempotency-Key.
  • Sync accounts get a mode, Lite, Direct or Relay, and POST /accounts/{id}/payouts pays a Lite merchant, in live only.
  • The onboarding webhooks sent to a Sync platform are now named account.onboarding_started, account.onboarding_email_verified, account.onboarding_account_selected, account.onboarding_kyc_submitted and account.connected, and can be subscribed to. Event catalogue

2026-09-15

Webhooks API Transfers

  • Webhook retries: every failed delivery except a 410 is retried with a growing wait, for up to 72 hours. It used to be five attempts over about sixteen minutes, and a 4xx was never retried. Retries
  • Idempotency: the stored response is replayed with Idempotent-Replayed: true, a second request arriving while the first still runs gets a 409, and the same key with another body gets a 422. Idempotency
  • Bank transfer payouts.

SDKs

  • PHP SDK 2.0.0, with breaking changes: it requires PHP 8.4; update() and delete() are removed from refunds and transfers, which the API never had; tax->reports() returns the report object, shield->addToBlocklist() the updated blocklist, and listen->config() and listen->auth() unwrap their envelope. Cursor pagination is followed by getNextPage() and autoPagingIterator(). PHP SDK

2026-09-11

Payments Transfers CLI

  • Djamo wallets, for payments and for beneficiaries.
  • DELETE /keys/current revokes the key wajub login created. wajub logout calls it.

2026-09-07

Transfers

  • Added sn.wave channel for transfers in Senegal.

2026-09-01

NEW API

New dated API version: 2026-09-01, with one breaking change: ledger_balance_before and ledger_balance_after are removed from the transfer object. Send X-Wajub-Version: 2026-08-01 to keep them. Versioning

The API accepted this version from 10 September: before that, X-Wajub-Version: 2026-09-01 answered 400.

2026-08-30

Payments

  • DELETE /customers/{id} anonymises the customer: the name is replaced, and the email, phone and date of birth are erased. customer.deleted carries the anonymised record.

2026-08-12

Payments

  • A charge the provider declines answers 402 with an error_code, insufficient_funds for instance, and the payment goes back to pending, ready to be charged again.

2026-08-11

Payments

  • A payment method is saved only with the payer's consent, sent as data.save_payment_method.

2026-08-10

Webhooks

  • payment.processing is emitted when a payment is handed to the operator.

2026-08-07

Webhooks

  • fee.charged is emitted when Wajub debits a fee. Fee debits are left out of balance.updated.

2026-08-01

NEW API

New dated API version: 2026-08-01, with one breaking change: complete replaces is_complete on the transfer object. Send X-Wajub-Version: 2025-01-01 to keep reading is_complete. Versioning

2026-07-24

Orchestration

  • A provider connection's priority now comes first: a lower-priority connection is never tried before a higher one, and cost and rules only decide between connections of the same priority.

2026-05-23

BETA Shield

Shield enters public beta: a risk score from 0 to 100 built on signal families (IP, velocity, card, device, blocklist, sanctions), configurable review_threshold/block_threshold, and a blocklist. Live only. See Shield →

2026-05-20

NEW Konsole

Konsole launch, the developer cockpit: Overview, API Logs, and Webhooks. Discover Konsole →

2026-04-03

BETA Tax

Tax in beta: automatic VAT calculation for CM, CI, SN, NG, GH, and KE, in inclusive and exclusive modes. See Tax →

2026-01-09

NEW Link

Link launch, code-free payment links with brand customization and conversion tracking. See Link →

What did you think of this content?