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-23

Sync Webhooks

  • New event account.onboarding_declined: the merchant declined the connection in the hosted onboarding. Nothing is connected and the invitation link keeps working; reason carries the merchant's words when they gave one. Like the other events of the merchant's own steps, it reaches only an endpoint subscribed to its name. Accounts, for Sync platforms

2026-10-17

BREAKING Sync

A change on every API version at once: it is not tied to a dated version, and X-Wajub-Version does not keep the earlier behaviour. It concerns the Sync deliveries that carry X-Sync-Event, those of the merchant's own steps, and the return to your page.

  • Breaking for a platform that verifies X-Sync-Signature: those deliveries no longer carry it. Verify X-Sync-Signature-V1 with X-Sync-Timestamp, as constructEvent does in the SDKs. Signature verification
  • Breaking for a platform that reads sync_uid: those deliveries name the connection only in account, the acc_… id, and carry none of Wajub's internal ids of the connection, the platform or the merchant. account.onboarding_started no longer carries the address the merchant opened the link from. Sync lifecycle deliveries
  • Breaking for a platform that reads sync_id on its return page: the merchant comes back with account, state and status only. Back on your page
  • Only a 2xx is a delivery: a redirect is not followed and counts as a failure. Each attempt goes to the endpoint as it is then, signed with its current secret, and an endpoint disabled or deleted in the meantime is sent nothing more.

API Webhooks Konsole

  • Resending webhooks by hand is no longer counted against a monthly allowance, on any plan. The same event goes again to the same endpoint at most once every 5 minutes, from Konsole or from POST /events/{id}/resend. The call skips an endpoint inside that delay, and answers 429 with error_code: event_resend_cooldown and a Retry-After header when every endpoint that has not accepted the event is. A 429 that is not a throttle
  • Konsole: Resend inside that delay says how many minutes are left, and Retry failed deliveries skips those deliveries and counts them in its message. Webhook tester

Link Orchestration

  • A payment link's own webhook counts as delivered only on a 2xx: any other answer, a redirect included, is logged as failed and retried, and a redirect is never followed. Its test send says only whether the endpoint accepted it. In the Dashboard
  • An alert rule's Slack or webhook target must be an https URL that does not point to a private or internal address: Wajub sends nothing to any other, and follows no redirect from it. Alerts

BREAKING API

A change on every API version at once: it is not tied to a dated version, and X-Wajub-Version does not keep the earlier behaviour.

  • Breaking for an integration that builds or edits a cursor: a list takes only a next_cursor or prev_cursor exactly as a response returned it. Any other value, a cursor made by hand or altered included, is a 422 on cursor. A cursor returned before this change is still read until 2026-11-13. Cursor pagination
  • Breaking for an integration that sends GET /balance a currency Wajub cannot convert to: currency takes your account's currency, or a currency Wajub has an exchange rate for, in upper or lower case. Any other code is a 422 on currency. Balance
  • Breaking for a platform that tells 403 from 404 with X-Sync: a connection that belongs to another platform answers 404 Sync account not found., as an id that does not exist does. Keys and access

API

  • A restricted key (rk. or rk_test.) used from a browser sets off the same security alert as a secret key: the account's owner and admins are emailed. For now the request still goes through; Wajub will refuse it with the same 403 later, so move those calls to your server. Private keys are blocked in the browser

2026-10-16

BREAKING API

API version 2026-10-12, the version of new accounts, also brings these changes. Earlier versions keep the previous behaviour, and webhooks follow the current version, 2026-10-11.

  • A payment no longer has theming. Earlier versions still read the key, null on a payment created from now on.
  • POST /webhooks and PUT /webhooks/{id} check each type of events against the event catalog: a type that does not exist, a misspelt one or a prefix such as payment.* answers 422 on events.N. * still takes every type, and an endpoint keeps the types it already has. Webhook endpoints
  • A Shield blocklist entry no longer carries team_id. Shield
  • The items of a payment made on an invoice's page end with the invoice-wide discount, when there is one: an item with kind: discount, name: Discount, a negative total, and quantity and unit_price at null. The Payment object

Versioning

API Payments

  • DELETE /payments/{id} answers 409 when the payment cannot be cancelled now: error_code: payment_in_progress while a charge is under way, the payer approving it on their phone or paying on the provider's page, and payment_partly_paid on a partial payment or one with instalments paid, to close with POST /payments/{id}/close. A processing payment with nothing under way can be cancelled, and so can one past its expires_at that has not expired yet. Cancelling a payment
  • Of two charges sent at once on one payment, the second answers 409 payment_in_progress. A payment whose charge is still under way when expires_at passes waits for that charge's outcome before it can expire, and in live a charge on a payment past its expires_at answers 400. Expiry
  • Some operators, such as Orange Money in Burkina Faso, take a payment only with a code the payer generates first. The charge then answers 202 with action: payer_otp and, in payer_otp, how to get the code: charge again with it in data.otp. After three refused codes, the charge answers 402 otp_attempts_exceeded. In sandbox, bf.orange asks for the code and accepts 123456. A code the payer generates
  • The payment object returns client to a secret or restricted key: where the payer paid from, the hosted checkout, Wajub.js, a mobile SDK or another app, as it reports itself, with its version and platform when they are known. The Payment object
  • POST /payments no longer keeps theming, on every version: the field is ignored, never refused.
  • otp_masked_contact holds only the masked contact, such as 080******78, and is empty when the provider names none. GET /payments/options gives each channel and operator logo_png, its logo as a PNG. Previewing the methods
  • On a charge, a Mobile Money number may start with 00 instead of +. The 0 that is part of a number in Côte d'Ivoire, Benin, Congo and Burkina Faso is kept, and Benin's and Gabon's renumberings are read in their new form. Phone number formats
  • Sandbox: Benin's ten-digit numbers (+2290190…, +2290191…, +2290197…) and Gabon's current blocks (+24174…, +24177…, +24162…, +24165…, +24166…) have test numbers, and 00 may stand for +. Test scenarios

API

  • POST /accounts, POST /tax/registrations, POST /customers/{id}/tax_ids and POST /disputes/{id}/messages honour Idempotency-Key: a repeated call answers what the first one did, for 24 hours. Idempotency
  • A path that encodes a slash or a backslash, %2F or %5C, answers 404. Error shape
  • A dispute's customer is the customer's cus_ id, even once that customer is deleted, and its transaction the payment's trx_ id. Either is left out when there is none.
  • On the versions before 2026-10-12, a channel of GET /channels and GET /providers reads the currency it pays in, or null for a channel that is not paid in one currency, such as a card or a crypto asset, instead of XAF for every channel. Coverage

API Link

  • single_use_link: true closes a link after its first successful payment and lets one payer pay through it at a time: another payer is told a payment is in progress, and the link opens again if that payment does not go through. A refund does not reopen it. Should a second payment still succeed, your team's owner, administrators and finance members get an e-mail naming the payment to refund. A link that already had single_use_link now closes after its first payment, except a digital product's, whose setting becomes single_use_access. How a link closes
  • single_use_access: true, for a digital product, lets each buyer download the file once; the link stays open to the next buyer. The buyer downloads through a link bound to their payment, never the file's own address, and a full refund stops the download. file_url on POST /links takes an address of your own: a file uploaded in the Dashboard answers 422. Digital products
  • A service's meeting link reaches the payer in the confirmation e-mail of their payment, with what to prepare, and is no longer on the page before payment.
  • link.expired is sent once, within a day after a link's expiry_date passes, unless the link is archived by then, with the link in data, to the endpoints that subscribe to it, and GET /events?type=link.expired lists it. Event catalogue

API Invoices

  • Line discounts come off an invoice's subtotal, total and amount_due, and a discount can no longer take a line below zero: 422 on items.N.discount_value. Open invoices with a discounted line are brought in line, which can lower their total and amount_due, never raise those of an invoice already sent, and turns one already paid up to its new total paid, with invoice.updated and invoice.paid. How a line is computed
  • The items of a payment made on an invoice's page give each line its total, net of its discount and tax included, on every version. The Payment object
  • global_discount_type and global_discount_value take a discount off the whole invoice, which returns them with has_global_discount. The receipt and the payment confirmation list it after the lines. An invoice-wide discount
  • Quotes and estimates made with POST /invoices are read, corrected, cancelled and deleted by their id, and GET /invoices?document_type=quote or estimate lists them. /send and /mark-paid answer 400 on them. Quotes and estimates
  • Your customers are reminded of unpaid invoices by e-mail before and after the due date, invoices sent through the API included, unless you turn reminders off in Settings → Invoice → Automatic Reminders, where you also pick the days. An invoice already sent from the Dashboard keeps the reminders already scheduled for it; your days take over for the days that follow. Reminders
  • Invoice and reminder e-mails are written in the invoice's language, their footer included.

SDKs

  • The next releases of the Flutter (1.6.0), React Native (1.5.0) and Android (1.3.0) SDKs guide the payer through a code they generate in their operator's menu, in a new state, PAYER_OTP_REQUIRED (CheckoutState.payerOtpRequired in Dart). A when or a switch that lists every state needs a branch for it. Every amount the sheet shows is what the payer pays, the Wajub fee included when they bear it, for the method or operator they pick. States

2026-10-15

NEW Plugins

  • PrestaShop module 1.0.1, for PrestaShop 8.1 to 9: Mobile Money and Card as payment options of their own, with Wajub's payment form inside the checkout (Classic and Hummingbird), and Other payment methods on Wajub's payment page, each shown only when your account offers it for the cart. The order is created when the customer places it and settled by the webhook, instalment payments included; a payment short of the total counts, and the customer pays the rest. An order PrestaShop splits into several packages is paid, refunded and cancelled as one. PrestaShop
  • PrestaShop module 1.0.1: Connect with Wajub gets the shop its key from the Dashboard, with the client type prestashop; the back office and the shop's e-mail address are told from 14 days before a key it created expires. Refunds both ways, from the order page and from the Dashboard; a refund Wajub or the module refuses makes no credit slip. One connection per shop of a multistore, and the customer's data for PrestaShop's GDPR module. Connect your app to Wajub
  • PrestaShop module 1.0.1: alerts in the back office for an environment chosen without its key, a key that expires, a shop open to the public still in Sandbox, Wajub missing from the checkout, and a scheduled task that does not run; the settings warn a shop not served over HTTPS in Live. Wajub · Sandbox shows in the back office's header while the shop takes test payments. Uninstalling keeps the record of the orders' Wajub payments, refunds and notes. Back-office alerts

2026-10-14

BREAKING API Transfers

A privacy change on every API version at once: it is not tied to a dated version, and X-Wajub-Version does not keep the earlier behaviour.

  • Breaking for an integration that shows registered_name after a name_mismatch: POST /beneficiaries/{id}/verify returns only the initials of the name the operator holds when it does not match the one you declared, P*** N***. A match still returns the full name. Checking the name on a wallet

BREAKING API Payments

A security change on every API version at once: it is not tied to a dated version, and X-Wajub-Version does not keep the earlier behaviour.

  • Breaking for an integration that reads a payment's customer with a publishable key (pk.): POST /payments returns in customer, shipping_address and billing_address only what the request sent, never the customer the account already has with that email or phone. GET /payments/{id}, and POST /payments/{id}, /splits and /confirm, return no customer, shipping_address or billing_address, and payment_method holds only channel. Read them from your server, with a secret or restricted key. Authentication
  • Breaking for an integration that reads the customer POST /customers returns with a publishable key: when the account already has a customer with that email or phone, the answer, still 200, holds its id and the fields the request sent. Read the stored customer from your server, with a secret or restricted key; those keys are unchanged. Authentication
  • Breaking for an integration that reuses an Idempotency-Key on POST /payments with another body: the whole body is compared, expires, customer, metadata, bearer, address and shipping included, and a change answers 422. On POST /payments, a key a publishable key used answers 422 to a secret or restricted key, and the other way round. Idempotency

API Payments

  • expires.in sent empty, blank or null gives POST /payments its default expiry, 24 hours. On PATCH /payments/{id}, it leaves the expiry as it was. Update a prepared payment
  • OPay (ng.opay) and PalmPay (ng.palmpay) take a number from any network, as Wave, Express Union, Djamo and Capitec Pay do. Direct API

CLI

  • CLI 1.7.4: a 406 Private Key Required, the answer to a publishable key (pk.) on a route that takes a secret one, such as wajub payments list or wajub customers retrieve, says the request needs a secret (sk.) or restricted (rk.) key, names where the publishable key came from (WAJUB_API_KEY, --api-key or the profile) and says how to put a secret key there, wajub login for a profile.

Dashboard

  • Changing a customer's details or status now needs a recent security verification, as revealing a key does: editing, blocking, unblocking, activating, deactivating or deleting a customer, one at a time or, editing aside, in bulk, and deleting one for good.
  • Deleting customers in bulk now anonymises them, as deleting one does.
  • Creating or editing a live customer with an email no longer fails with an error.
  • A beneficiary's type is shown read-only on its edit page: it is set when the beneficiary is created, and the page no longer offers a change it did not make.

Components

  • Wajub.js 1.8.1: @wajub/angular is new, for Angular 17 and later, installed with @wajub/js 1.8.0 or later beside it. provideWajub() and WajubService load Wajub.js. Standalone components render the hosted checkout, <wajub-checkout-embed>, and each field group, <wajub-card> to <wajub-crypto>, or any of them with <wajub-component>, and injectConfirmPayment() submits a field. Options are inputs, callbacks are outputs, and onBeforeSubmit is the beforeSubmit input. The package works with zone.js and zoneless, and with Angular SSR. @wajub/js, @wajub/react, @wajub/vue and @wajub/svelte move to 1.8.1 with no other change. Angular
  • Wajub.js 1.8.0: loader: 'skeleton' keeps your page still while a payment field loads: a placeholder drawn in your fields' style holds the form's height until the form is ready. componentReserveHeight(type, variant, fields), on wajub and as a synchronous export of @wajub/js, gives that height. componentEstimatedHeight(type, variant, fields), from @wajub/js, gives an estimate that is the same on the server and at hydration. readyTimeout reports a field that is not ready in time to onLoadError and loaderror, with ready_timeout. Loading without a layout shift
  • Wajub.js 1.8.0: appearance.inherit, true or { input, label, button, background }, takes the look of your page's fields, label, button and background, under the keys you set, in payment fields, mount() and open(). wajub.appearanceFrom(), also on the Wajub() client and in @wajub/js, returns what it reads, {} from @wajub/js when the page runs an older Wajub.js. Taking your page's look
  • Wajub.js 1.8.0: onStateChange, onResize and onRedirect at create(), for the statechange, resize and redirect events, and on() and off() type each event's payload through ComponentEventMap. confirmPayment({ timeout }) no longer counts the time the payment waits on the payer, such as APPROVAL_REQUIRED, OTP_REQUIRED or 3DS_REQUIRED, nor the time the payer spends on the provider's page. A field taken off the page without unmount() rejects it with component_unmounted, and a field mounted again starts from INITIATED. Locking your checkout while the payment runs
  • Wajub.js 1.8.0: update({ appearance }), on a field, mount() or open(), changes the keys you give and keeps the others. A key given as undefined or null is cleared, and appearance: undefined clears it all, back to your Dashboard branding. On mount() and open(), a new theme preset replaces the previous one and what it brought. update({ layout }) lays out an embedded checkout's methods again, inline and in the overlay. Changing it after mount
  • @wajub/react, @wajub/vue and @wajub/svelte 1.8.0, which need @wajub/js 1.8.0: every option above is a prop, and BankTransferComponent and CryptoComponent join the field components. CheckoutEmbed applies a change of appearance, theme, locale or layout in place, and loads again only for a new sessionId or embedOrigin. useConfirmPayment reads the session when you confirm, from a ref or a getter in Vue and a store or a getter in Svelte, and a sessionId passed to confirm() wins. The Svelte field components take every handler as a prop, and type every prop. Mobile Money and Card in your own checkout
  • Changes for the React, Vue and Svelte field components in 1.8.0: loader defaults to 'skeleton', so pass loader="none" to keep the frame as it comes. minHeight has no fixed default: the component holds the room the form needs, so minHeight={0} is no longer needed with variant="fields", and a minHeight you pass is kept. A new variant, fields, loader, readyTimeout, collectAddress, addressMode or collectName mounts a new field in the same container. The appearance prop is sent whole: a key you remove from it is cleared. Changing a handler never reloads the form. Through a framework
  • A payment field whose session already ended shows its notice, then sends ready when the session was paid, else loaderror with session_terminal_expired, session_terminal_cancelled or session_terminal_failed: start a new payment. A field that cannot tell which page it is in sends loaderror with component_origin_missing. When the form does not come up
  • Wajub.js 1.7.2: with variant: 'fields', a component takes the height of its inputs; with the React, Vue and Svelte field components, pass minHeight 0. rules values the checkout refuses are dropped. Inside your own payment method box
  • update({ appearance }) on an embedded checkout lasts through the payer's next steps. The page checkout() sends the payer to takes your appearance from its first render. A colorScheme you give applies from the first paint on every page, and hides the checkout's light and dark button. The color and the font of a .Input rule reach the typed text, card fields included, and the color of a .Label rule reaches the card holder's label. A Google Font the checkout loads reaches the card fields, and without fontSizeBase the fields are 16px on a phone or a tablet, so iOS does not zoom in on focus. OPay and PalmPay take a number from any network, as Wave, Express Union and Djamo do. Appearance
  • Wajub.js 1.7.1: appearance.theme set to light, dark, system or auto is read as colorScheme, system meaning auto, when you give no colorScheme. The presets stripe, night, flat and none are unchanged. Presets
  • A colorScheme you give wins over the payer's device and the choice they saved, in a payment field and in the embedded checkout. variables.fontFamily, which loads a Google Font the checkout knows, fontSizeBase, which sizes the fields, at least 16px on a phone or a tablet, with labels and hints a step smaller, colorText, colorTextSecondary, colorTextPlaceholder and borderRadius now style the fields. inputBackgroundColor and inputBorderColor style the fields alone. rules for .Input and .Label reach the fields, and the check on rules values applies on every path, update({ appearance }) included. Rules
  • ready fires once the component's form is on screen, or at once when it has no form to show. In the Mobile Money form, each operator of a channel that carries several, such as MTN and Orange in Cameroon, is a choice of its own with its logo, and with fields.operator: 'detect' the list shows when the number does not tell the operator. A wallet held on any network, such as Wave, Express Union or Djamo, takes any number. Inside your own payment method box

API Sync

  • A subaccount names the merchant who claimed it connected_merchant, on every API version. slave, the same object, is deprecated and gone from API version 2026-10-12, which also drops the id of master and connected_merchant and a channel's uid, a repeat of its id, and identifies a provider of GET /providers by prv. and its slug. Versioning

2026-10-13

BREAKING API Payments

A security change on every API version at once: it is not tied to a dated version, and X-Wajub-Version does not keep the earlier behaviour.

  • Breaking for an integration that calls these routes with a publishable key (pk.): they now answer 406 Private Key Required. GET /payments, DELETE /payments/{id}, POST /payments/{id}/close and GET /payments/{id}/refunds; GET /customers, GET, PUT and DELETE /customers/{id}, and its block, unblock, activate and deactivate; every /links and /invoices route. Make those calls from your server, with a secret or restricted key. A publishable key still creates, charges, confirms and reads a payment by its id, reads GET /payments/options, creates a customer with POST /customers, and reads the reference data. Authentication
  • Breaking for a restricted key that holds only payment.* scopes: POST /payments/{id}/close with outcome: refund also takes refund.write, as POST /refunds does, and GET /payments/{id}/refunds also takes refund.read. Add the refund scope the key needs. Restricted key scopes
  • Breaking for a platform whose connection holds payments without refunds: with X-Sync, GET /payments/{id}/refunds and POST /payments/{id}/close with outcome: refund also take the connection's refunds capability, as /refunds does: a connection with payments alone now gets 403. Account capabilities

API Payments

  • POST /payments/{id}/activate takes reference and description, which replace the ones given when the payment was prepared, before your order existed. Left out, the prepared ones stay. Activate a prepared payment
  • PATCH /payments/{id} checks the new amount as POST /payments does: outside the currency's limits it answers 422 on amount, and when accept leaves no way to pay it, 422 on accept. It takes expires.in, from 5 to 43 200 minutes from now, and the payment keeps that expiry once activated. Update a prepared payment

Dashboard CLI Plugins

  • Approving wajub login or an app's connection, such as the WordPress plugin's Connect with Wajub, takes the owner, an Administrator or a Developer (a Sandbox User, for a sandbox key only), and a recent security verification code. The page always starts on Sandbox, whatever the app asked for, and the approver picks the environment. Install and authenticate
  • The approver chooses how long an app's key lasts: 90 days, the default, or no expiry. A wajub login key always lasts 90 days. The owner and every Administrator get an email for each key a pairing creates.
  • Settings → Developers → API keys shows when each key stops working: Expires … or Expired …. A key with no expiry shows no date. Environments & API keys
  • Transferring the team, confirming a payout, and revealing an API key or copying its .env lines ask for a security verification code, from your authenticator app or sent by SMS or email, like rotating or deleting a key. Team and security

Components

  • Wajub.js 1.7.0: frameTitle, passed to a component's create() or update(), names its iframe for screen readers in your page's language.
  • Wajub.js 1.7.0: when the payment continues on the provider's own page, the component emits redirect with { url, method } and Wajub.js takes your page there. Back from that page, the payer lands on the session's Wajub page, in the component's language, which shows the outcome and then sends the payer to the payment's callback with status, reference and trxref, or keeps the outcome on screen when there is no callback. A page on an earlier Wajub.js keeps the earlier behaviour. When the payment continues on the provider's page
  • A submission you ask for with submit() or confirmPayment() that a component refuses, such as a Mobile Money number empty or not valid, a card left incomplete or rejected, card fields still loading, a bank transfer with no IBAN or account number or a crypto payment with no asset, emits error with submission_failed and a message for the payer, and confirmPayment() rejects with it. Enter pressed in a form that is not ready shows the reason in the component only. A submission while the payment runs does nothing, so nothing is charged twice. The OTP, PIN, phone approval, USSD and 3-D Secure screens give their height to resize, and a card window the browser blocked after onBeforeSubmit leaves a Continue on … button in the component. Submitting

Plugins

  • WordPress plugin 1.4.0: two new WooCommerce payment methods, Mobile Money and Card, each with Wajub's payment form inside your checkout, classic and Blocks. Both stay off until you turn them on, so an existing store sees no change. WordPress
  • WordPress plugin 1.4.0: Connect with Wajub, in the setup assistant and in Settings, gets the site its key once you approve it in the Dashboard, with nothing to copy; pasting keys stays available. The key's expiry shows in the admin, and the site's administration address gets an email from 14 days before it.
  • WordPress plugin 1.4.0, behaviour change: each environment pays with its own key only. A site in Sandbox with only a live key no longer takes real payments with it, and takes none until you add a sandbox key or switch to Live. On update, a production site in that state switches to Live once and says so; a local, development or staging copy stays in Sandbox.
  • WordPress plugin 1.4.0: a WooCommerce customer pays only what is left. A payment short of the order's total, such as an instalment payment closed early, counts toward it, and the customer pays the rest from the order's payment page. Refunds from WooCommerce are split across the order's payments. WordPress

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.
  • 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

  • A Mobile Money charge above the per-transaction ceiling answers 422, in sandbox as in live. 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, 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;
    • GET /links: an unknown type; GET /events: a type no endpoint receives;
    • GET /channels: an unknown country; GET /countries: an unknown region; GET /currencies: a type other than fiat, crypto or all, read whatever its case.

    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. Reading a transfer no longer counts in the transfers share. 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 returns 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. 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 can answer 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 (Checkout — 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, 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 that fails 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 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
  • 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. Shield API

Orchestration

  • 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. 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 HTTP client (at least 90 s), 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. With two checkouts on a page, each 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. 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 Node.js and C# 2.0.0, PHP 2.0.1, Ruby 1.3.0, Python, Go and Java 1.2.1: 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 and Android 1.2.1, and Flutter from its next release, 1.6.0: an error that trying again cannot fix, such as an ended session, is no longer marked retryable. React Native and Android cancel the payment when the payer closes the sheet before submitting, and Flutter's 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. 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. WordPress
  • WordPress plugin 1.2.1: a partial refund started from WooCommerce is recorded once. MemberPress sends the transaction receipt.
  • 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. Webhook payloads are still shaped for the latest version, whatever your account's. Versioning

API Sync Shield Konsole

  • A dispute you accept or close now carries metadata.resolution set to accepted. Disputes
  • POST /beneficiaries/{id}/verify now asks the operator for every account. Outside Sync Relay, check again any beneficiary checked before this release 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: React Native from 1.4.1 and Android from 1.2.1, their first published releases with them, and Flutter from its next release, 1.6.0. Cards on the payment provider's own secure page, and one-tap wallets such as Djamo. 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. 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
  • 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, recorded events and webhook deliveries, and the copies a Sync platform received. 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.
  • 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. 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.
  • 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 and /tax 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. 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 and a repeated call answers 200 instead of 404. Deleting a payer
  • On a charge made with a secret key from your own checkout, call Wajub.signals() (Wajub.js 1.6.0) in the payer's browser and pass the result unchanged as data._client. 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
  • Importing wajub.mjs from js.wajub.com 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 release of the new wajub listen; 1.6.0 was not released.

2026-10-02

CLI

  • CLI 1.6.0: wajub listen hands your handler 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. Payout writes (creating beneficiaries, transfers and batches) are capped at 20 a minute on every plan, Enterprise included. 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 for up to 72 hours, a 4xx included. 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 and shield->addToBlocklist() the updated blocklist. Cursor pagination is followed by getNextPage() and autoPagingIterator(). PHP SDK

2026-09-11

Payments Transfers CLI

  • Djamo wallets, for payments and for beneficiaries.
  • wajub logout revokes the key wajub login created.

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

What did you think of this content?