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.
| Integration | Checkout | Refunds from WordPress |
|---|---|---|
| WooCommerce | Redirect, inline, overlay, separate Mobile Money and Card methods, Blocks | Yes |
| Easy Digital Downloads | Redirect | Yes, each refund EDD makes |
| GiveWP 3.x | Redirect | Yes |
| GiveWP below 2.18 | Redirect | Yes, on status change |
| Charitable | Redirect | Yes |
| Tutor LMS | Native Ecommerce checkout | Yes |
| LifterLMS | Redirect | Yes |
| LearnDash | Shortcode button | No order model to refund |
| MemberPress | Redirect | Yes |
| Gravity Forms | Redirect | Yes |
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.
Subscriptions are not supported anywhere
WooCommerce Subscriptions, MemberPress recurring billing and GiveWP recurring donations all need a stored payment method the gateway can charge again. This plugin has none, so a subscription product does not offer Wajub at checkout.
Requirements
| Component | Minimum | Note |
|---|---|---|
| WordPress | 6.2 | Tested through 7.0 |
| PHP | 8.1 | The plugin shows an admin notice and stops below this |
| WooCommerce | 8.0 | Only if you use it. Tested through 11.0 |
| HTTPS | Required | The return address of every payment needs it, and so does a live webhook |
| A Wajub account | Required | Sandbox 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
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
Or install it from the command line
WP-CLI takes the zip path or URL directly.
- 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 plugin install ./wajub.zip --activate
wp plugin list --name=wajub --fields=name,status,versionThe folder has to be named wajub
A folder named otherwise, such as wajub-main, breaks the Gravity Forms feed and the plugin's
styles. Rename the folder to wajub before activating.
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
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
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
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.
| Rule | Detail |
|---|---|
| Who approves | The 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 |
| Live | The approval page offers it once your account is activated for live payments, to a member whose role is not limited to Sandbox |
| 15 minutes | A 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 time | A 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.
A copy of the site never deletes the store's key
A staging site cloned from the store carries the store's key, and the record that a connection
gave it. Deleted from the copy, that key would stop the store's payments. So a replaced key is
deleted only when WP_ENVIRONMENT_TYPE is production and the site runs at the address that
connected it. A copy tells you to delete the old key yourself if nothing uses it any more, and
sends no expiry e-mail: the site that connected sends its own.
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.
| Key | Environment | Accepted |
|---|---|---|
sk_test. | Sandbox | Yes |
rk_test. | Sandbox | Yes, as a restricted key |
sk. | Live | Yes |
rk. | Live | Yes, as a restricted key |
pk_test., pk. | Either | No: 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.
| Option | Holds |
|---|---|
wajub_test_mode | Sandbox switch, on by default |
wajub_secret_key_test | sk_test. or rk_test. key |
wajub_webhook_secret_test | whsec_test_ secret |
wajub_secret_key | sk. or rk. key |
wajub_webhook_secret | whsec_ secret |
To keep secrets out of the database, read them from the environment with WordPress's
pre_option_ filter.
<?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:
wajub listen --forward-to https://shop.test/wp-json/wajub/v1/webhookAdd --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.
| What | Value |
|---|---|
| URL | https://your-site.com/wp-json/wajub/v1/webhook |
| Method | POST |
| Signature header | X-Wajub-Signature: v1=<hmac> |
| Timestamp header | X-Wajub-Timestamp |
| Signed payload | The timestamp, a dot, then the raw body |
| Algorithm | HMAC SHA-256 with your webhook secret |
| Tolerance | 300 seconds |
No matching secret means the delivery is rejected
A delivery that no saved signing secret verifies is refused with 403. There is no unsigned
mode. An order stuck on pending after a successful payment is almost always this. Wajub, Settings shows when the last delivery arrived, and what to do
when one was refused.
You can prove the endpoint works without leaving your terminal. Sign a payload the same way Wajub does and post it.
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
| Mode | Where the customer pays |
|---|---|
redirect | On the Wajub hosted checkout, then back to your thank you page |
inline | On your checkout page, in an embedded frame |
overlay | On 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.
| Method | Gateway id | Takes | Logos |
|---|---|---|---|
| Wajub Mobile Money, titled Mobile Money at checkout | wajub_mobile_money | Mobile Money only. The payer picks the operator from a list | MTN MoMo, Orange Money, Wave, Moov Money, Airtel Money, M-Pesa |
| Wajub Card, titled Card at checkout | wajub_card | Cards only | Visa, 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
The payer picks the method
The form loads in the method's payment box. Nothing is charged until the payer places the order.
- 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 inmetadata.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
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.
Serve the store over HTTPS
Bringing that payer back takes a return address Wajub accepts: https://, or http://localhost
and http://127.0.0.1 in Sandbox. On a store served over plain HTTP elsewhere, a payer taken to
their provider's page stays there. The setup assistant and Wajub, Settings warn such a
store in Live, and a production site still in Sandbox.
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 key | Holds |
|---|---|
_wajub_reference | Your reference, wc_<order id>_<timestamp> |
_wajub_payment_id | The Wajub payment id, trx_… |
_wajub_dispute_status | The 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 status | GiveWP donation status |
|---|---|
succeeded | Completed |
cancelled, expired | Cancelled |
failed | Failed |
refunded, partially_refunded | Refunded |
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.
The LMS buttons stand down when WooCommerce is active
Both the LearnDash button and the LifterLMS pricing table option render nothing when WooCommerce is installed. On a site that sells courses through WooCommerce products, that is the correct behaviour, the WooCommerce gateway already covers it. On a site that uses WooCommerce for something unrelated, the button will not appear and the shortcode is the way to place 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.
The feed offers a subscription type that does nothing
The transaction type dropdown on the feed lists Subscription alongside Products and Services. A subscription feed takes the first payment and never charges again. Pick Products and Services.
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.
amountnumberoptionalcurrencystringoptionaldefault : XAFmoderedirect | inline | overlayoptionaldefault : redirecttypepayment | donationoptionaldefault : paymentamountsstringoptionaldescriptionstringoptionalproduct_idnumberoptionalcontent_idstringoptionalbutton_textstringoptionaldefault : Pay <amount>classstringoptional[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.
[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().
<?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())
));
}Full page caching breaks the shortcode form
The form carries a WordPress nonce, valid for at most 24 hours. A page cached longer than that
shows the visitor an error asking them to refresh. Exclude pages carrying [wajub_pay] from your
page cache, or keep the cache lifetime under a day.
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
| Hook | Arguments | Fires when |
|---|---|---|
wajub_payment_complete | $reference, $data, $source, $sourceId | A payment succeeded |
wajub_payment_failed | $reference, $data, $source, $sourceId | A payment failed, was cancelled or expired |
wajub_refund_succeeded | $paymentRef, $data, $source, $sourceId | A refund succeeded, made in the Wajub dashboard or in WordPress, once per refund |
wajub_refund_complete | $paymentRef, $data, 'woocommerce', $orderId | A refund succeeded on a WooCommerce order |
wajub_dispute_received | $eventType, $data | Any dispute event |
wajub_webhook_received | $eventType, $data | Every verified event, once the plugin has handled it |
wajub_webhook_missing_secret | $reason, $environment | An 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, $environment | An 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_events | The event list | The 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_enabled | true | The 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.
<?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.
<?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.
<?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.
<?php
add_filter('wajub_default_phone_country_code', static fn (): string => '221');What the API client gives you
| Method | Does |
|---|---|
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
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
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
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
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
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
| Symptom | Cause | Fix |
|---|---|---|
| Wajub is missing from the WooCommerce checkout | No key for the environment in use. Sandbox does not use the live key | Add the key, with Connect with Wajub or by pasting it, or switch to Live |
| Wajub takes no payments. in the admin | Sandbox is selected with only a live key saved | Add 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 days | Reconnect, and choose No expiry if the checkout must not stop |
| Orders stay on pending after a successful payment | The webhook secret field is empty, or the endpoint is not registered | Every 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 page | The form could not load: blocked, or refused by the embedding restriction of your account | Add 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 page | The store is served over plain HTTP | Serve it over HTTPS |
| The shortcode form returns an invalid request error | An expired nonce served from a full page cache | Exclude the page from the cache, or shorten the cache lifetime below 24 hours |
| The Gravity Forms feed does not appear | The plugin folder is not named wajub | Rename it to wajub and reactivate |
| Inline checkout sends the payer to Wajub's payment page | Wajub.js did not load | Check the console for a blocked request to js.wajub.com. The payment page is the intended fallback |
| An amount is a hundred times too large | Minor units, out of habit | Wajub amounts are in the major unit. Send 25000 for twenty five thousand francs, not 2500000 |
| Nothing is logged | Debug logging is off | Turn 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.