Skip to content

WordPress and WooCommerce

One plugin, nine integrations, and the hooks to build your own.

The Wajub plugin adds Wajub as a payment method to the e-commerce, donation, LMS, membership and form plugins already on your WordPress site, and gives you shortcodes for everything else. One set of keys and one webhook cover them all.

What the plugin does

Nine integrations ship a real gateway. Each one turns on when the matching plugin is active.

IntegrationCheckoutRefunds from WordPress
WooCommerceRedirect, inline, overlay, separate Mobile Money and Card methods, BlocksYes
Easy Digital DownloadsRedirectYes, each refund EDD makes
GiveWP 3.xRedirectYes
GiveWP below 2.18RedirectYes, on status change
CharitableRedirectYes
Tutor LMSNative Ecommerce checkoutYes
LifterLMSRedirectYes
LearnDashShortcode buttonNo order model to refund
MemberPressRedirectYes
Gravity FormsRedirectYes

Five more appear in the admin with a shortcode badge. They have no payment gateway to plug into, so you drop [wajub_pay] on the page instead: WPForms, Bookly, The Events Calendar, WP Simple Pay and Amelia. WP Crowdfunding runs through WooCommerce, so enabling the WooCommerce integration covers it.

Requirements

ComponentMinimumNote
WordPress6.2Tested through 7.0
PHP8.1The plugin shows an admin notice and stops below this
WooCommerce8.0Only if you use it. Tested through 11.0
HTTPSRequiredThe return address of every payment needs it, and so does a live webhook
A Wajub accountRequiredSandbox is enough to start. Live needs an activated account

Wajub sends a payer back only to an https:// address. Sandbox makes one exception, for a site on your own computer: http://localhost and http://127.0.0.1. A site served over plain HTTP at any other address cannot start a payment, except through the Mobile Money and Card methods, which then cannot bring a payer back from their provider's page.

The plugin supports WooCommerce's High Performance Order Storage and the Blocks cart and checkout.

Install the plugin

Install the plugin from the WordPress.org directory: in WordPress admin, go to Plugins, Add New Plugin, search for Wajub, click Install Now, then Activate. WP-CLI does the same with wp plugin install wajub --activate. WordPress then offers each new version under Plugins, and installs it by itself once you turn on automatic updates for it.

You can also install it from its zip, which the plugin's page on WordPress.org offers for download.

  1. 1

    Upload the zip

    In WordPress admin, go to Plugins, Add New Plugin, Upload Plugin, pick the zip and install it. Activate when the upload finishes.

  2. 2

    Or install it from the command line

    WP-CLI takes the zip path or URL directly.

  3. 3

    Or drop the folder over SFTP

    Unzip locally and upload the folder to wp-content/plugins/, then activate it from the plugins screen.

From the zip, WP-CLI covers the same install in one line, and the second command confirms the plugin is active under the right folder name.

WP-CLI
wp plugin install ./wajub.zip --activate

wp plugin list --name=wajub --fields=name,status,version

Activation creates three pages, Payment successful, Payment not completed and Payment processing, each holding one [wajub_pay_message] shortcode. Then the setup assistant opens, unless the site already has a key or you activated several plugins at once, the whole network, or from WP-CLI.

The three pages are ordinary pages: edit them and move them into your theme's design. Wajub, Settings lets you pick other pages instead.

Connect your Wajub account

The setup assistant, Wajub, Setup, takes four steps: Connect, Webhooks, Checkout, and Test and go live. It stays in the Wajub menu until the environment in use has a key. After that, Wajub, Settings opens it again with Run the setup assistant again.

The site needs one secret key per environment. Let Wajub create it with Connect with Wajub, or paste your own.

Connect with Wajub

You approve the site in the Wajub Dashboard and Wajub creates its key: nothing to copy, and the key never reaches the browser. Only WordPress administrators see the buttons.

  1. 1

    Start from WordPress

    Click Connect with Wajub (Sandbox) or Connect Live in the setup assistant, or Connect Sandbox, Connect Live (Reconnect Sandbox, Reconnect Live once a key is saved) in Wajub, Settings. The site shows a code, such as K7QD-9XHT, and opens the Dashboard's approval page in a new tab. Keep the WordPress page open.

  2. 2

    Approve in the Dashboard

    Check that the Dashboard shows the same code. The page starts on Sandbox whichever button you clicked: choose the environment there. It also starts on a restricted key with the permissions the plugin needs ticked, payments, refunds and webhooks, read and write; choose full access if you prefer. Choose how long the key lasts, 90 days or No expiry, then click Authorize and enter your security code.

  3. 3

    Back in WordPress

    The site saves the key, registers the webhook with it, and says what it saved. It warns you when you approved another environment than the one you asked for, when a restricted key may lack a permission the plugin needs, and when the key belongs to another business than the one the site used.

RuleDetail
Who approvesThe owner, an Administrator or a Developer of your Wajub account, or a Sandbox User for a sandbox key only, after a security code. Wajub e-mails the owner and every Administrator about every key created this way. Approve only a code your site shows you
LiveThe approval page offers it once your account is activated for live payments, to a member whose role is not limited to Sandbox
15 minutesA request waits 15 minutes for approval. The site must then collect the key within 15 minutes of the approval, or Wajub deletes it and you start again
One request at a timeA request started in another tab or window replaces the first, and the first page says so

Any app can ask for a key the same way: see Connect your app to Wajub. add_filter('wajub_pairing_enabled', '__return_false'); hides Connect with Wajub, and keys are then pasted.

When a connected key expires

A key that lasts 90 days stops working on its date. Wajub, Settings, the setup assistant and the connection card of Wajub, Overview show that date. From 14 days before it:

  • the site's administration e-mail address gets one e-mail per key, Sandbox or Live;
  • for the key of the environment in use, a notice on the other admin screens, Your Wajub key expires soon., offers to connect again in one click, and can be hidden for 7 days. Once the date has passed, the notice reads Your Wajub key has expired. until the key is replaced, and the connection status says the key expired.

Choose No expiry for a store whose checkout must not stop.

Reconnecting

Wajub, Settings says, for each environment, the day Connect with Wajub gave its key, for as long as that key is the one saved. Reconnecting replaces the environment's key, and the key a previous connection gave is deleted. When it cannot be, the site tells you to delete it in the Dashboard, under Settings → Developers → API keys. A key you pasted is never deleted, and deleting the plugin deletes no key: remove the keys Connect with Wajub created in the Dashboard.

Paste your keys

Open Paste keys instead in the setup assistant, or use the key fields of Wajub, Settings. Copy the keys from the dashboard.

KeyEnvironmentAccepted
sk_test.SandboxYes
rk_test.SandboxYes, as a restricted key
sk.LiveYes
rk.LiveYes, as a restricted key
pk_test., pk.EitherNo: a publishable key is refused

A restricted key needs the payments, refunds and webhooks scopes, read and write: payment.read, payment.write, refund.read, refund.write, webhook.read and webhook.write. Without the webhook ones, the site cannot register its webhook and gives you the steps to do it by hand.

Each key is checked with Wajub before it is kept. A key pasted in the other environment's field, a publishable key, or a key Wajub refuses is not saved, and the message says why. A key saved while Wajub could not be reached says so: use Test connection a moment later. A saved key shows masked, its prefix and its last four characters. The browser never gets a key.

Test connection in Wajub, Settings checks the key in use with Wajub.

Keys from the environment

The keys and the Sandbox switch are WordPress options.

OptionHolds
wajub_test_modeSandbox switch, on by default
wajub_secret_key_testsk_test. or rk_test. key
wajub_webhook_secret_testwhsec_test_ secret
wajub_secret_keysk. or rk. key
wajub_webhook_secretwhsec_ secret

To keep secrets out of the database, read them from the environment with WordPress's pre_option_ filter.

Keys from the environment, in a must-use plugin
<?php
// wp-content/mu-plugins/wajub-keys.php

foreach ([
    'wajub_secret_key' => 'WAJUB_SECRET_KEY',
    'wajub_secret_key_test' => 'WAJUB_SECRET_KEY_TEST',
    'wajub_webhook_secret' => 'WAJUB_WEBHOOK_SECRET',
    'wajub_webhook_secret_test' => 'WAJUB_WEBHOOK_SECRET_TEST',
] as $option => $env) {
    add_filter("pre_option_{$option}", static function () use ($env) {
        $value = getenv($env);

        return $value === false || $value === '' ? false : $value;
    });
}

// The keys live in the environment: Connect with Wajub could not replace them.
add_filter('wajub_pairing_enabled', '__return_false');

Returning false lets WordPress read the stored option as usual, so the fields in the admin still work on a machine where the variables are not set.

Sandbox and Live

Each environment pays with its own key only: Sandbox with the sandbox key, Live with the live key. Sandbox takes test payments, and no real money moves. Live takes real payments.

Sandbox without a sandbox key takes no payment at all, even with a live key saved. Every Wajub method is then hidden at WooCommerce checkout, the other integrations and the pay buttons refuse to start a payment, and a red notice in the admin, Wajub takes no payments., offers Add a sandbox key or Switch to Live. WooCommerce's Wajub settings read Sandbox — no payments.

Switching is guarded both ways. Wajub, Settings refuses Live while no live key is saved, and asks you to confirm before it saves Live. It keeps Live when you switch back to Sandbox without a sandbox key, since the site would stop taking payments.

The environment shows on every Wajub screen. While Sandbox is in use, administrators see Wajub · Sandbox in the admin bar, in the admin and on the storefront, and a notice warns when a production site is still in Sandbox.

While Sandbox is in use, live webhooks still settle the live orders placed before you switched, except on a copy whose WP_ENVIRONMENT_TYPE is not production. A Sandbox delivery never changes an order while Live is in use.

MemberPress, LifterLMS, GiveWP 3, Easy Digital Downloads 3 and Charitable record a Wajub payment as a test or a live one from the key Wajub takes it with, not from their own test mode, so a real payment is never recorded as a test. LifterLMS shows no test mode setting for Wajub: Wajub, Settings chooses.

Register the webhook

The webhook is not optional. It is the only thing that confirms a payment when the customer closes the tab, loses signal after approving on their phone, or pays through a channel that settles minutes later. Every integration in this plugin relies on it, and a payer's return never marks an order failed on its own.

The plugin registers it for you. Set up webhooks automatically in the setup assistant, or Register automatically in Wajub, Settings, creates the endpoint for each environment that has a key and saves the signing secret Wajub returns. Connect with Wajub does the same for the environment it connects. The webhook subscribes to the payment, refund and dispute events the plugin acts on; the wajub_webhook_events filter changes the list.

Wajub cannot reach a site on a local or private address, and a live webhook needs https://. For those, the assistant gives the steps to do it by hand: register the URL in the Dashboard, then paste the signing secret in Wajub, Settings. To test from your computer, wajub listen from the Wajub CLI forwards your account's events to the site, with no public address and no inbound port:

Forward events to a local site
wajub listen --forward-to https://shop.test/wp-json/wajub/v1/webhook

Add --insecure-tls if the site uses a self-signed certificate, and --live with a live key. Then paste the signing secret the command shows into the assistant, under Set it up by hand instead.

WhatValue
URLhttps://your-site.com/wp-json/wajub/v1/webhook
MethodPOST
Signature headerX-Wajub-Signature: v1=<hmac>
Timestamp headerX-Wajub-Timestamp
Signed payloadThe timestamp, a dot, then the raw body
AlgorithmHMAC SHA-256 with your webhook secret
Tolerance300 seconds

You can prove the endpoint works without leaving your terminal. Sign a payload the same way Wajub does and post it.

Send a signed test event
SECRET='whsec_test_your_secret_here'
BODY='{"id":"evt_local_test","event":"payment.succeeded","data":{"reference":"wc_42_1757000000","status":"succeeded","amount":25000,"currency":"XAF","metadata":{"source":"woocommerce","source_id":"42"}}}'
TS=$(date +%s)

SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -r | cut -d" " -f1)

curl -i -X POST https://your-site.com/wp-json/wajub/v1/webhook \
  -H "Content-Type: application/json" \
  -H "X-Wajub-Timestamp: $TS" \
  -H "X-Wajub-Signature: v1=$SIG" \
  -d "$BODY"

A valid signature returns {"received":true}, and a bad one 403. The test event settles nothing: the plugin completes an order only with a payment it created for that order.

WooCommerce

WooCommerce is the deepest integration. It gets three checkout modes for the Wajub method, separate Mobile Money and Card methods, Blocks support, refunds in both directions, dispute notices on the order, and a retry path for a payment that did not go through.

Enable the gateway

Go to WooCommerce, Settings, Payments and enable Wajub. Its default title is Mobile Money or card. With the Mobile Money or Card method on, it becomes Other payment methods. Every Wajub method hides itself from the checkout while the environment in use has no key, so if none appears there, the key is missing.

Pick a checkout mode

ModeWhere the customer pays
redirectOn the Wajub hosted checkout, then back to your thank you page
inlineOn your checkout page, in an embedded frame
overlayOn your checkout page, in a modal

Redirect is the default and the one to start with. It is the only mode that works with JavaScript disabled. Inline and overlay work on both the classic and the Blocks checkout. To take Mobile Money and cards in the checkout's own payment boxes instead, see Separate Mobile Money and Card methods.

Inline and overlay load Wajub.js from https://js.wajub.com and keep the customer on the checkout page. Payment form language applies to every mode. Payment form colours (system, light or dark) applies to inline and overlay, where System follows the customer's device, and to the Mobile Money and Card forms, where it follows your site. If Wajub.js cannot load, the customer pays the same order on Wajub's payment page.

Separate Mobile Money and Card methods

Beside the Wajub method, two WooCommerce payment methods of their own hold Wajub's payment form inside their payment box, in the classic and the Blocks checkouts. A Mobile Money payer types the number to pay with and approves the payment on their phone; a card payer enters their card. Nobody leaves the store.

MethodGateway idTakesLogos
Wajub Mobile Money, titled Mobile Money at checkoutwajub_mobile_moneyMobile Money only. The payer picks the operator from a listMTN MoMo, Orange Money, Wave, Moov Money, Airtel Money, M-Pesa
Wajub Card, titled Card at checkoutwajub_cardCards onlyVisa, Mastercard

Both are off until you turn them on, so an existing store sees no change. Choose Separate methods in the setup assistant's Checkout step, where Mobile Money and Card come ticked as your account offers them for the store's currency, or enable them in WooCommerce, Settings, Payments. Each has its own title, description and logos, the brands' own.

A Mobile Money payer chooses the operator from a list that shows each operator's logo, with the operator of their number already selected. Recognise the operator from the number instead of showing the list (operator_from_number, off by default), under Operator in the Mobile Money method's settings, hides the list: the form reads the operator from the number, and shows the list only when the number does not tell.

The form opens in the language set for the Wajub method, and takes your checkout's own font, sizes, colours, rounding, and light or dark look. Payment form colours, also set on the Wajub method, forces that look when set to Light or Dark; System, the default, follows your site.

The Wajub method becomes Other payment methods

The separate methods take payments of their own kind only. With Mobile Money or Card on, the Wajub method, kept on beside them, becomes Other payment methods: it offers only what they do not, such as PayPal, wallets or a bank transfer, its payment is restricted to those kinds, and it shows only when your Wajub account has one for the order, so no way to pay shows twice. A title or description you wrote yourself is kept. In the assistant, Also offer the other methods your Wajub account has (PayPal, wallets…) is ticked by default; unticked, the assistant turns the Wajub method off. Choosing Redirect, Overlay or Inline later turns the separate methods off and the Wajub method back on.

Each method shows only when your Wajub account offers its kind of payment for the order.

  1. 1

    The payer picks the method

    The form loads in the method's payment box. Nothing is charged until the payer places the order.

  2. 2

    The payer places the order

    WooCommerce creates the order, then the payment goes through the form. It carries the order's reference (wc_<order id>_<timestamp>), reads "Order #123" in the Dashboard, and has the order's id in metadata.order_id. A form left open too long, or replaced in another tab, is refreshed with a message instead, and nothing is charged: the payer checks it and places the order again.

  3. 3

    The payment goes through, or not

    A declined payment can be tried again in the same form, with no new order. Use another payment method gives the checkout back, and Continue on the secure Wajub page finishes the same payment on Wajub's page. A payer who leaves while Wajub is still confirming goes to the order page, which shows the outcome once it is known, and is never offered a second payment.

Webhooks, the payer's return, the order confirmation page, Check status in the order's Wajub payment box, and refunds from WooCommerce work for these orders as for the Wajub method.

Wajub's payment page takes over without JavaScript, on the order pay page, and when the form cannot load; it then offers that method only. If you restricted where Wajub can be embedded (Settings, Checkout embedding in the Dashboard), add your store's address there, or the form does not load.

A Mobile Money number the form refuses, or a window the browser blocked, is explained in the payment box, and the payer can fix it and try again. A payer whose Mobile Money operator takes over the whole page comes back to the order.

When a payment does not cover the order

An order is paid once the Wajub payments made for it cover its total, in its currency and in one environment. Each payment counts for what it collected, less what was refunded of it. A fee the customer bore never makes up for a total raised after the payment started. A restricted key needs refund.read for refunds to be counted: without it, the order gets a note.

A payment in another currency than the order's counts for nothing, and the customer is still asked for the order's total. The order gets one note per such payment, with what it collected in its own currency and to refund it from the Wajub dashboard, and Check status says so too.

A payment falls short when you close an instalment payment early in the Wajub dashboard, or edit the order after its payment started. What it collected still counts. The order stays pending with a note of what is left, and its confirmation page shows Part of your order is paid with a Pay the rest button. The customer pays the rest from the order's payment page, which you can send with Send order details to customer. The new payment asks for what is left only, and Wajub's page describes it as Order #N: X already received; Y left to pay. A customer never pays twice for the same order.

While Wajub holds part of an order, WooCommerce's cleanup of unpaid orders leaves it alone, and another payment method is refused for it: "Part of this order is already paid. Pay the rest with …". The cleanup never cancels an order either while one of its payments went through but could not be counted yet: the order stays pending with a note.

The other integrations (GiveWP, Easy Digital Downloads, Charitable, LifterLMS, Tutor LMS, MemberPress, Gravity Forms) ask one payment to cover what is due on its own. One that falls short completes nothing, and the purchase stays pending with a note, except in MemberPress and Tutor LMS, where only the plugin's debug log says so.

Refunds

Refunds work in both directions, for orders paid with any of the three methods.

A refund started in WooCommerce is sent to Wajub. A refund started in the Wajub dashboard creates the matching WooCommerce refund, without moving money a second time. Neither is ever recorded twice, and an amount larger than what is still refundable is refused.

A refund of an order paid by several payments is split across them, newest first, each up to what it can still refund. If Wajub refuses one part after others went through, WooCommerce drops its refund and the parts already made are recorded on their own. A refund is refused for an order never paid in full, an order paid in the other environment, such as a refund started in Sandbox on an order paid in Live, or, for an order paid by several payments, an amount larger than what they hold. A refund made in the Wajub dashboard while the order was still unpaid is not recorded in WooCommerce: the customer pays that amount again.

When Wajub refuses a refund as more than it can refund, and nothing of it went through, the plugin reads the refunds of the order's payments: one made in the Wajub dashboard whose webhook never reached the site is recorded as its webhook would have, and the refund box says what was caught up.

The order meta the plugin writes

Meta keyHolds
_wajub_referenceYour reference, wc_<order id>_<timestamp>
_wajub_payment_idThe Wajub payment id, trx_…
_wajub_dispute_statusThe last dispute event seen for this order

Read them with $order->get_meta() in your own code. The reference is the one to log, because it is what appears in the Wajub dashboard and in every webhook.

Disputes

A dispute.created, dispute.won or dispute.lost event adds an order note with the reason and amount, records the dispute in _wajub_dispute_status, and emails the site admin with the response deadline. Other dispute events are noted on the order without an email.

Retry after a failure

When a payment fails or the confirmation times out, the customer gets a retry link. It never cancels the order. An order that can still be paid opens its own payment page, and one already paid opens its confirmation. Only an order WooCommerce has cancelled, when its stock hold ran out, has its products and variations put back in the cart, with a notice explaining what happened.

An order cancelled, when its stock hold runs out or by you, has its Wajub payment that nobody started paying cancelled with Wajub too, a moment later, so that it can no longer go through. One being paid, one that went through, or one paid in instalments is not: Wajub cannot stop a payment the operator or the bank is confirming, and the order says to refund it from the Wajub Dashboard should it take money.

Donations with GiveWP

Both generations of GiveWP are covered, with nothing different to configure.

Enable Wajub in Donations, Settings, Payment Gateways. Donors pay on the hosted checkout and come back to your success page.

Wajub statuses map onto Give statuses rather than collapsing into a single failure:

Wajub statusGiveWP donation status
succeededCompleted
cancelled, expiredCancelled
failedFailed
refunded, partially_refundedRefunded

A refund from the GiveWP admin is sent to Wajub. Recurring donations are not supported.

GiveWP is redirect only

The donation form redirects to the hosted checkout. If you want an inline or overlay donation experience, build the page with the [wajub_pay type="donation"] shortcode instead, which supports all three modes. GiveWP does not record that payment: it shows in Wajub, Transactions only.

Donations with Charitable

The gateway appears in Charitable on its own, with nothing to fill in on its settings screen: it uses the keys from Wajub, Settings.

Donors pay on the hosted checkout. Once the payment is confirmed, the donation is marked complete and the donor lands on the receipt page. A refund from the Charitable admin is sent to Wajub once.

Easy Digital Downloads

Wajub appears at the EDD checkout as Mobile Money or card. Customers pay on the hosted checkout, then the EDD payment is completed with a note carrying the reference, or marked failed with the reason.

With EDD 3, each refund you make in EDD, full or partial, is sent to Wajub with its own amount. The refund that refunds the order in full, or a status set to Refunded where EDD makes no refund order, asks Wajub for what is left of the payment.

Courses with Tutor LMS, LifterLMS and LearnDash

The three LMS plugins get three different levels of integration, because they expose three different amounts of surface.

Tutor LMS gets a native gateway: Wajub appears in the normal Tutor Ecommerce checkout and enrollment happens through Tutor's own order flow. Refunds issued from Tutor are sent to Wajub. If Tutor Ecommerce is switched off, a [wajub_pay] button appears on the course page instead.

LifterLMS gets a real gateway too. A paid order redirects to the hosted checkout, and once the payment is confirmed the transaction is recorded on the LifterLMS order and the student enrolled.

LearnDash has no order model this plugin can drive, so it gets a payment button beside the course payment buttons, and access is granted once the payment is confirmed. Nothing is refundable from WordPress here, so refund from the Wajub dashboard instead.

The course buttons (LearnDash, the LifterLMS pricing option, and Tutor's [wajub_pay] when Tutor Ecommerce is off) ask a logged out visitor to log in, or to create an account when registration is open, and a payment enrolls the account that made it.

Memberships with MemberPress

The gateway handles one-time membership payments. A purchase redirects to the hosted checkout, and once the payment is confirmed the MemberPress transaction completes and the receipt is sent.

Recurring billing is not implemented. A membership with a subscription price will not be charged again automatically.

Gravity Forms

The add-on works like any other Gravity Forms payment feed: create a feed on the form, map the amount and the email, and Wajub handles the rest.

Submissions redirect to the hosted checkout. Once the payment is confirmed, the entry is marked Paid and gform_post_payment_status fires, so your own feeds and notifications run. A refund from the entry is sent to Wajub once.

Any other plugin, through shortcodes

Three shortcodes cover every plugin without a gateway, and every custom page you build yourself. No key ever reaches the browser.

A payment button

[wajub_pay] renders a small form asking for an email and an optional name, then starts the payment in the mode you chose.

amountnumberoptional
Amount in the major unit. 25000 XAF is twenty five thousand francs.
currencystringoptionaldefault : XAF
Three letter code. Anything else falls back to XAF.
moderedirect | inline | overlayoptionaldefault : redirect
Anything else falls back to redirect.
typepayment | donationoptionaldefault : payment
A donation renders an amount field the donor fills in.
amountsstringoptional
Preset amounts for a donation, such as 1000,2500,5000.
descriptionstringoptional
Shown on the checkout and stored on the payment. Trimmed to 500 characters.
product_idnumberoptional
A WooCommerce product id. Its price and name override amount and description.
content_idstringoptional
Marks the payment as unlocking a piece of content.
button_textstringoptionaldefault : Pay <amount>
The submit button label. A donation reads Donate.
classstringoptional
One extra CSS class on the wrapper.
Three ways to place the button
[wajub_pay amount="25000" currency="XAF" description="Consultation" mode="redirect"]

[wajub_pay product_id="482" mode="overlay" button_text="Buy now"]

[wajub_pay type="donation" currency="XAF" mode="inline" button_text="Support us"]

Locked content

[wajub_pay_content] wraps content that only appears once the reader has paid. It renders the same form until then, defaulting to the overlay mode so the page does not move under them.

A paywalled section
[wajub_pay_content content_id="masterclass-2026" amount="15000" currency="XAF"]
The full recording, the slides, and the worksheet live here.
[/wajub_pay_content]

Access is remembered two ways: on the user account when the buyer is logged in, and for thirty days in the browser the payment was started from when they are not. A logged out buyer who changes browser will have to pay again, so this is the right tool for a digital download or a single article, not for a membership. MemberPress or LifterLMS handle that properly.

The price is the one written in the shortcode, and the payer cannot change it. Give each price its own content_id: paying for one opens every block that shares it. Editing the page asks payers on an older copy to reload it.

The result message

[wajub_pay_message] renders the success, failure or processing message. It is what the three pages created at activation contain, and it stays empty on a direct visit.

A payment that collected less than it asked, such as an instalment payment closed early, shows Partial payment received, with what was paid of what was due and whom to contact, rather than Payment received. It unlocks nothing: locked content stays locked. A Gravity Forms payment returns to the same pages.

Shortcodes inside a theme template

Outside the editor, run them through do_shortcode().

In a template file
<?php
$amount = (float) get_post_meta(get_the_ID(), 'ticket_price', true);

if ($amount > 0) {
    echo do_shortcode(sprintf(
        '[wajub_pay amount="%s" currency="XAF" description="%s" mode="overlay"]',
        esc_attr((string) $amount),
        esc_attr(get_the_title())
    ));
}

Build your own integration

Three pieces of the plugin are yours to use in your own code: its hooks, its API client and its signed callback token.

The hooks

HookArgumentsFires when
wajub_payment_complete$reference, $data, $source, $sourceIdA payment succeeded
wajub_payment_failed$reference, $data, $source, $sourceIdA payment failed, was cancelled or expired
wajub_refund_succeeded$paymentRef, $data, $source, $sourceIdA refund succeeded, made in the Wajub dashboard or in WordPress, once per refund
wajub_refund_complete$paymentRef, $data, 'woocommerce', $orderIdA refund succeeded on a WooCommerce order
wajub_dispute_received$eventType, $dataAny dispute event
wajub_webhook_received$eventType, $dataEvery verified event, once the plugin has handled it
wajub_webhook_missing_secret$reason, $environmentAn event arrived with no secret configured. $environment is the environment it claims (live, test or empty); its body, which nobody verified, is not passed
wajub_webhook_invalid_signature$reason, $environmentAn event failed signature or timestamp checks (same arguments)
wajub_default_phone_country_code'237'A nine digit phone number needs a country code
wajub_webhook_eventsThe event listThe plugin registers its webhook
wajub_fields_prepare_limits['ip' => 10, 'site' => 30]The Mobile Money and Card box may ask for its payment, per minute, per visitor address and for the whole site
wajub_pairing_enabledtrueThe admin offers Connect with Wajub; false hides it
wajub_charitable_gateway_name'Wajub'Charitable lists the gateway; return another name to show

The last five are filters. The rest are actions. $source and $sourceId come straight from the metadata you set when creating the payment, which is what makes your own source name work, but whoever creates a payment can set them: check the payment against your own records before acting on it. A delivery the site does not act on, such as one signed for the environment not in use or a repeat, runs none of the actions.

Fulfil your own record

Set your own metadata.source when you create the payment, then act on it. The hook runs when the webhook confirms the payment, rather than when the browser assumes it.

Marking your own booking as paid
<?php

add_action('wajub_payment_complete', static function (
    string $reference,
    array $data,
    string $source,
    $sourceId
): void {
    if ($source !== 'bookings' || empty($sourceId)) {
        return;
    }

    $booking = get_post((int) $sourceId);
    if (! $booking || $booking->post_type !== 'booking') {
        return;
    }

    update_post_meta($booking->ID, 'payment_status', 'paid');
    update_post_meta($booking->ID, 'wajub_reference', $reference);
    update_post_meta($booking->ID, 'paid_amount', (float) ($data['amount'] ?? 0));

    do_action('bookings_confirmed', $booking->ID);
}, 10, 4);

Create a payment from your own code

Client::getInstance() gives you a client with the key of the environment selected in Wajub, Settings, and only that one: in Sandbox without a sandbox key it has no key, isConfigured() is false, and every call throws. Amounts are in the major unit.

Starting a payment and redirecting
<?php

use WajubPay\API\Client;
use WajubPay\Security\CallbackToken;

function bookings_start_payment(int $bookingId, float $amount, string $email): string
{
    $callback = add_query_arg([
        'bookings' => 'wajub_return',
        'booking_id' => $bookingId,
        '_wajub_token' => CallbackToken::generate($bookingId),
    ], home_url('/'));

    $result = Client::getInstance()->createPayment([
        'amount' => $amount,
        'currency' => 'XAF',
        'description' => sprintf('Booking #%d', $bookingId),
        'reference' => 'booking_' . $bookingId . '_' . time(),
        'callback' => $callback,
        'customer' => ['email' => $email],
        'metadata' => [
            'source' => 'bookings',
            'source_id' => (string) $bookingId,
        ],
    ]);

    $parsed = Client::parsePaymentResponse($result);
    // The Wajub id (trx_...): the API reads a payment by its id, not by your reference.
    update_post_meta($bookingId, 'wajub_payment_id', $parsed['id']);

    return $parsed['authorization_url'];
}

parsePaymentResponse() flattens the API envelope into id, reference, session_id, authorization_url and status. Send the customer to authorization_url for a redirect checkout, or hand session_id to wajub.mount() for an embedded one.

The callback URL is where the customer lands on the way back, and it follows the HTTPS rule. It is a browser redirect, so treat it as a hint about which page to show and never as proof of payment. Verify the token, then verify the payment.

Handling the return safely
<?php

use WajubPay\API\Client;
use WajubPay\Security\CallbackToken;

add_action('init', static function (): void {
    $action = isset($_GET['bookings']) ? sanitize_text_field(wp_unslash($_GET['bookings'])) : '';
    if ($action !== 'wajub_return') {
        return;
    }

    $bookingId = absint($_GET['booking_id'] ?? 0);
    $token = sanitize_text_field(wp_unslash($_GET['_wajub_token'] ?? ''));

    if (! $bookingId || ! CallbackToken::verify($bookingId, $token)) {
        wp_safe_redirect(home_url('/'));
        exit;
    }

    $paymentId = (string) get_post_meta($bookingId, 'wajub_payment_id', true);
    $status = 'pending';

    try {
        $payment = Client::getInstance()->retrievePayment($paymentId);
        $status = (string) (Client::extractTransaction($payment)['status'] ?? 'pending');
    } catch (\Throwable) {
        // The webhook is still the source of truth. Show a waiting page.
    }

    wp_safe_redirect(Client::isSucceededStatus($status)
        ? home_url('/booking-confirmed/')
        : home_url('/booking-pending/'));
    exit;
});

CallbackToken::generate() ties the token to the id, and to an optional second value such as an order key. It is what stops a visitor from confirming someone else's booking by editing the id in the URL. Always pass the same second argument to verify() that you passed to generate().

Change the default phone country code

A nine digit phone number with no country code is assumed to be Cameroonian. Change it once for the whole site.

A Senegalese default
<?php

add_filter('wajub_default_phone_country_code', static fn (): string => '221');

What the API client gives you

MethodDoes
createPayment(array $params)POST /payments, returns the raw envelope
retrievePayment(string $id)GET /payments/{id}. Pass the Wajub id (trx_…): the API does not find a payment by your reference
cancelPayment(string $id)DELETE /payments/{id}, with the Wajub id as well
createRefund(string $paymentId, ?float $amount, ?string $reason, string $currency)POST /refunds
paymentOptions(string $currency, float $amount, string $country = '', array $accept = [])GET /payments/options: the channels a payment of that currency and amount could be paid with, as the account offers them
isConfigured(), isTestMode()Whether the client has a key, and whether it is the sandbox one

Failures throw a typed exception: AuthenticationException, PermissionException, NotFoundException, RateLimitException, InvalidRequestException or ApiConnectionException, all extending ApiException with readonly errorCode, httpStatus and body properties.

Transactions

Wajub, Transactions lists every payment and refund the site has recorded, whatever integration it belongs to, so a WooCommerce order, a GiveWP donation and a shortcode payment sit side by side. It has search, filters and a CSV export. Wajub, Overview sums succeeded volume by currency, so a site taking both XAF and GHS sees two figures.

Deleting the plugin deletes this list and the plugin's settings, keys and secrets. Deactivating does not. Orders, donations and entries keep their Wajub details, and the webhook endpoints and the keys Connect with Wajub created stay in your Wajub account. Export anything you need before you delete the plugin.

Go live

  1. 1

    Test the whole path in sandbox

    Place a real order with a sandbox Mobile Money number and watch the order reach a paid state without touching the admin. Test a cancellation too, and a refund.

  2. 2

    Confirm the webhook is firing

    The order should complete even if you close the tab immediately after approving. If it only completes when you return to the site, the webhook is not configured and you are relying on the callback.

  3. 3

    Add the live credentials

    Click Connect Live, or paste the live secret key, then let the plugin register the live webhook. Live and sandbox endpoints are separate.

  4. 4

    Switch to Live

    Choose Live in Wajub, Settings, or in the setup assistant's last step, and confirm. The admin bar no longer shows Wajub · Sandbox.

  5. 5

    Take one real payment

    A small one, on a real phone, then refund it. It is the only test that exercises the live keys, the live webhook and your fulfilment together.

Account activation and the KYC that unlocks live keys are on Going live.

Troubleshooting

SymptomCauseFix
Wajub is missing from the WooCommerce checkoutNo key for the environment in use. Sandbox does not use the live keyAdd the key, with Connect with Wajub or by pasting it, or switch to Live
Wajub takes no payments. in the adminSandbox is selected with only a live key savedAdd a sandbox key, or Switch to Live to take real payments with the live key
Your Wajub key expires soon.The key came from Connect with Wajub and lasts 90 daysReconnect, and choose No expiry if the checkout must not stop
Orders stay on pending after a successful paymentThe webhook secret field is empty, or the endpoint is not registeredEvery delivery is refused with 403 until the secret matches. Register automatically, or send the signed test request above
The Mobile Money or Card form never shows, and the payer goes to Wajub's pageThe form could not load: blocked, or refused by the embedding restriction of your accountAdd the store's address under Settings, Checkout embedding in the Dashboard, and check the console for a blocked request to js.wajub.com
A card payer stays on their provider's pageThe store is served over plain HTTPServe it over HTTPS
The shortcode form returns an invalid request errorAn expired nonce served from a full page cacheExclude the page from the cache, or shorten the cache lifetime below 24 hours
The Gravity Forms feed does not appearThe plugin folder is not named wajubRename it to wajub and reactivate
Inline checkout sends the payer to Wajub's payment pageWajub.js did not loadCheck the console for a blocked request to js.wajub.com. The payment page is the intended fallback
An amount is a hundred times too largeMinor units, out of habitWajub amounts are in the major unit. Send 25000 for twenty five thousand francs, not 2500000
Nothing is loggedDebug logging is offTurn on Log Wajub requests and webhook events in Wajub, Settings, or define WP_DEBUG. Output goes to the WooCommerce logger under the wajub source when WooCommerce is active, otherwise to error_log

For anything else, turn on debug logging, reproduce, and send the log with the transaction reference to support. Never send a key.

What did you think of this content?