WordPress and WooCommerce
One plugin, nine integrations, and the hooks to build your own.
The Wajub plugin turns a WordPress site into a payment surface. It detects the e-commerce, donation, LMS, membership and form plugins already installed, registers itself as a payment method inside each of them, and falls back to shortcodes for everything it does not have a native gateway for. One set of keys, one webhook, every integration.
What the plugin does
Nine integrations ship a real gateway. The plugin loads each one only when it detects the matching plugin, so an install with WooCommerce alone never loads the GiveWP or LMS code.
| Integration | Detected through | Checkout | Refunds from WordPress |
|---|---|---|---|
| WooCommerce | WooCommerce class | Redirect, inline, overlay, Blocks | Yes |
| Easy Digital Downloads | EDD_VERSION | Redirect | Yes, on status change |
| GiveWP 3.x | givewp_register_payment_gateway | Redirect | Yes |
| GiveWP below 2.18 | GIVE_VERSION | Redirect | Yes, on status change |
| Charitable | CHARITABLE_VERSION | Redirect | Yes |
| Tutor LMS | TUTOR_VERSION | Native Ecommerce checkout | Yes |
| LifterLMS | LLMS_PLUGIN_FILE | Redirect | Yes |
| LearnDash | LEARNDASH_VERSION | Shortcode button | No order model to refund |
| MemberPress | MEPR_VERSION | Redirect | Yes |
| Gravity Forms | GFForms class | Redirect | Yes |
Five more appear in the admin with a shortcode badge. They have no gateway API the plugin can
hook 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. Version 1.2.0
removed the subscriptions flag from the WooCommerce gateway for that reason, so a
subscription product will not offer Wajub at checkout instead of failing silently on the first
renewal.
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 webhook and the return URL both need it |
| A Wajub account | Required | Sandbox keys are enough to start |
The plugin declares itself compatible with both WooCommerce features that break older gateways, High Performance Order Storage and the Blocks cart and checkout, so you do not have to stay on the legacy order tables to use it.
Install the plugin
The plugin is distributed as a zip rather than through the WordPress.org directory, so there is no search box install and no automatic update. Get the current build from your Wajub contact.
- 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.
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
The Gravity Forms add-on identifies itself by the path wajub/wajub.php, and the plugin
builds its own asset URLs from the same folder. A zip that expands to wajub-wordpress or
wajub-main breaks the Gravity Forms feed and leaves the frontend CSS unreachable. Rename the
folder to wajub before activating.
Activation does three things. It creates three pages, Payment Successful, Payment Failed and
Payment Processing, each holding one [wajub_pay_message] shortcode. It creates the
wp_wajub_transactions table. It schedules a rewrite rules flush so the REST routes answer
immediately.
The three pages are ordinary pages. Edit them, style them, move them into your theme's design.
The plugin only needs their ids, which it stores in wajub_success_page_id,
wajub_failure_page_id and wajub_callback_page_id.
Connect your Wajub account
Go to Wajub, Settings. The plugin keeps two sets of credentials, sandbox and live, and a switch that decides which set every request uses.
| Option | Holds | Used when |
|---|---|---|
wajub_test_mode | Sandbox switch, on by default | Always, it picks the pair below |
wajub_secret_key_test | sk_test. key | Test mode is on |
wajub_webhook_secret_test | whsec_test_ secret | Test mode is on |
wajub_secret_key | sk. key | Test mode is off |
wajub_webhook_secret | whsec_ secret | Test mode is off |
wajub_debug_log | Debug logging switch | Writes to the WooCommerce logger, or error_log |
A Wajub secret key is a prefix, a dot, then 96 characters. Live keys start with sk., sandbox
keys with sk_test.. Copy them from
the dashboard. The plugin never asks for
a publishable key: the browser gets a session token created by your server, never a key.
An empty sandbox field falls back to the live key
The API client reads the sandbox key in test mode, and if that field is empty it falls back to the live key rather than failing. A site that looks like it is in sandbox then charges real money. Fill both key fields, or leave the live field empty until you are ready to go live.
Nothing reads an environment variable on its own, but WordPress gives you the hook for it. The
core pre_option_ filter short circuits any option read, so you can keep secrets out of the
database entirely.
<?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;
});
}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.
The dashboard at Wajub calls GET /payments once per page load to show whether the keys
work. Connected means the key is valid, Error means the API refused it, Not configured means the
field is empty.
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.
Copy the URL from Wajub, Settings, or build it yourself.
| 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 |
Paste it in the Wajub dashboard under webhook endpoints, copy the signing secret it gives you back into the matching field in Wajub, Settings, and save.
No secret means every delivery is rejected
The handler returns 403 and does nothing at all when the webhook secret field for the
current environment is empty. There is no unsigned mode. An order stuck on pending after a
successful payment is almost always this.
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}. A bad one returns 403. Replaying the same
id returns {"received":true} without doing the work twice, because the handler stores every
event id it has seen for a week.
Two details worth knowing. The handler accepts both type and event as the event name key,
which matters because the Wajub wire format uses event. And it routes work by
metadata.source and metadata.source_id, the two fields every integration in this plugin
writes when it creates the payment. That pair is what tells the webhook which order, donation or
entry to complete.
WooCommerce
WooCommerce is the deepest integration. It gets three checkout modes, Blocks support, refunds in both directions, dispute notices on the order, and a retry path that rebuilds the cart.
Enable the gateway
Go to WooCommerce, Settings, Payments and enable Wajub. The gateway hides itself from the checkout until a secret key is saved, so if it does not appear there, the keys are missing.
Pick a checkout mode
| Mode | What the SDK calls | Where the customer pays |
|---|---|---|
redirect | authorization_url | On the Wajub hosted checkout, then back to your thank you page |
inline | wajub.mount() | On your checkout page, in an embedded frame |
overlay | wajub.open() | On your checkout page, in a modal |
Redirect is the default and the one to start with. It has the fewest moving parts and it is the only mode that still works with JavaScript disabled.
Inline and overlay load Wajub.js from https://js.wajub.com and keep
the customer on the checkout page. Two extra settings apply to them: a locale, and a theme of
system, light or dark.
The mode labels in the settings screen are stale
The dropdown still says the inline and overlay modes embed on the order pay page. Since 1.2.0 they embed on the checkout page itself and the customer never navigates. The help text under the field is the accurate one.
How inline and overlay stay on the page
Classic checkout and Blocks checkout take different routes to the same result, and both fall back to a working page if the JavaScript never runs.
On classic checkout, WooCommerce fires checkout_place_order_success on the checkout form with
the AJAX result, just before it reads result.redirect and navigates. The plugin listens,
rewrites that redirect to a same document fragment, and mounts the payment widget in place.
On Blocks checkout, onCheckoutSuccess observers are awaited before WooCommerce decides where to
go, so the plugin returns a promise that stays pending while the customer pays and resolves with
the real destination.
If either script fails to load, WooCommerce follows the original redirect to the order pay page, where the same widget is rendered server side. The customer still pays, they just take one navigation to get there.
While the widget is open, the page polls wajub/v1/embed-status every three seconds for up to
ten minutes. That endpoint asks the API for the real payment status, completes the order when it
reads a success, and hands back a retry URL when it reads a failure.
Refunds
Refunds work in both directions.
A refund started in WooCommerce calls POST /refunds and marks the Wajub refund id on the order
so the confirming webhook does not record it twice. A refund started in the Wajub dashboard
arrives as a refund.succeeded webhook and creates the matching WooCommerce refund with
refund_payment set to false, because the money has already moved.
The guard that makes partial refunds safe is the _wajub_refund_<id> order meta. A webhook whose
refund id is already marked is skipped, and an amount larger than what is still refundable is
rejected rather than clamped.
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_session_id | The authorization token used by inline and overlay |
_wajub_mode | The mode the order was created with |
_wajub_dispute_status | The last dispute event seen for this order |
_wajub_refund_<id> | Marks one Wajub refund as already recorded |
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 finds the order by reference or
payment id, writes _wajub_dispute_status, adds an order note with the reason and amount, and
emails the site admin with the response deadline. Intermediate dispute events are recorded 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. Following it cancels the pending order, refills the cart with the same products and variations, and sends them back to checkout with a notice explaining what happened. The link carries a nonce, so it cannot be shared or replayed.
Donations with GiveWP
Both generations of GiveWP are covered, and the plugin picks the right one at load time.
GiveWP 3 and up register through givewp_register_payment_gateway under the id wajub-givewp.
Enable it in Donations, Settings, Payment Gateways. Donors are redirected to the
hosted checkout and come back through a secure gateway route that verifies the donation, sets
the donation status and sends them to your success page.
GiveWP below 2.18 uses the older give_payment_gateways filter and a give-listener callback.
Nothing to configure differently.
Wajub statuses map onto Give statuses rather than collapsing into a single failure:
| Wajub status | GiveWP donation status |
|---|---|
succeeded | Complete |
canceled, cancelled, expired | Cancelled |
abandoned | Abandoned |
failed, rejected | Failed |
refunded, partially-refunded | Refunded |
Refunding a donation from the GiveWP admin calls the Wajub API. 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.
Donations with Charitable
The gateway extends Charitable_Gateway and registers itself automatically. It reads the keys
from Wajub, Settings, so the Charitable gateway settings screen has nothing to fill in.
Donors are redirected to the hosted checkout. On return, the plugin verifies the payment against
the API, checks that the stored reference and the source_id both match the donation, and only
then marks it complete and sends the donor to the receipt page. Refunds from the Charitable admin
call POST /refunds once and mark the donation so a second attempt is a no-op.
Easy Digital Downloads
Wajub appears at the EDD checkout as Wajub (Mobile Money, Card). There is no card form to render, so the gateway suppresses it.
The flow creates a pending EDD payment first, then the Wajub payment, then redirects. On return the plugin verifies against the API and either publishes the payment with a note carrying the reference, or marks it failed with the reason.
Refunds are driven by EDD itself. Moving a payment to refunded or partially_refunded calls
the Wajub API once, and a _wajub_refund_api_done meta flag stops a second call.
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. It plugs into Tutor Ecommerce through
tutor_payment_gateways, so Wajub appears in the normal Tutor checkout and enrollment happens
through Tutor's own order flow. Refunds issued from Tutor call the API. If Tutor Ecommerce is
switched off, a [wajub_pay] button appears on the course page instead.
LifterLMS gets a real gateway too, extending LLMS_Payment_Gateway and registered through
lifterlms_payment_gateways. A paid order redirects to the hosted checkout, and the confirming
webhook records the transaction on the LifterLMS order and enrolls the student.
LearnDash has no order model this plugin can drive, so it gets a payment button appended to
the course payment buttons, and access is granted on the wajub_payment_complete hook. Nothing
is refundable from WordPress here, so refund from the Wajub dashboard instead.
The LMS buttons stand down when WooCommerce is active
Both the LearnDash button and the LifterLMS pricing table option check for WooCommerce first and render nothing when it 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 registers through mepr-gateway-paths and handles one-time membership payments. A
purchase redirects to the hosted checkout, and the return callback verifies the payment,
completes the MemberPress transaction and sends the receipt notice.
Recurring billing is not implemented. A membership with a subscription price will not be charged again automatically.
Gravity Forms
The add-on builds on Gravity Forms' payment add-on framework, so it behaves like every other payment feed you have configured: create a feed on the form, map the amount and the email, and Wajub handles the rest.
Submissions redirect to the hosted checkout. The return callback verifies the payment against the
API, checks the stored reference and the source_id against the entry, then marks the entry Paid
and fires gform_post_payment_status so your own feeds and notifications run.
Refunding from the Gravity Forms entry calls the Wajub API once, guarded by a
wajub_refund_api_done entry meta.
The feed offers a subscription type that does nothing
The transaction type dropdown on the feed lists Subscription alongside Products and Services, because that is the standard Gravity Forms field. Wajub has no renewal path, so 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. They all go through one public REST route that creates the payment server side, so 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 : paymentdescriptionstringoptionalproduct_idnumberoptionalcontent_idstringoptionalbutton_textstringoptionaldefault : Pay with Wajubclassstringoptional[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 on a transient keyed by the content id and the buyer's email for thirty days 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 result message
[wajub_pay_message] renders the success, failure or processing message. It is what the three
pages created at activation contain, and it prints nothing at all unless the URL carries a
reference parameter, so it stays invisible on a direct visit.
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, and a nonce is valid for at most 24 hours. A page cached
longer than that serves an expired nonce and the REST route answers 403 with a message asking
the visitor to refresh. Exclude pages carrying [wajub_pay] from your page cache, or keep the
cache lifetime under a day.
Build your own integration
Everything above is built on the same three pieces you can use directly: an API client, an HMAC callback token, and a set of hooks fired from the webhook handler.
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, abandoned, rejected or expired |
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, after routing |
wajub_webhook_missing_secret | $rawPayload | An event arrived with no secret configured |
wajub_webhook_invalid_signature | $rawPayload | An event failed signature or timestamp checks |
wajub_default_phone_country_code | '237' | A nine digit phone number needs a country code |
The last one is a filter. 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.
Fulfil your own record
Set your own metadata.source when you create the payment, then act on it. The hook runs inside
the webhook, which is the only place a payment is confirmed rather than assumed.
<?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 configured client using whichever key set the test mode
switch selects. Amounts are in the major unit, and the client rounds them for zero decimal
currencies on its own.
<?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);
update_post_meta($bookingId, 'wajub_reference', $parsed['reference']);
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. 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;
}
$reference = (string) get_post_meta($bookingId, 'wajub_reference', true);
$status = 'pending';
try {
$payment = Client::getInstance()->retrievePayment($reference);
$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() is an HMAC over the id, optionally salted with a second value such as
an order key, signed with your webhook secret. 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 $reference) | GET /payments/{reference} |
cancelPayment(string $reference) | DELETE /payments/{reference} |
createRefund(string $paymentId, ?float $amount, ?string $reason, string $currency) | POST /refunds |
refundByStoredIds(string $paymentId, string $reference, …) | Resolves the trx_ id first, then refunds |
testConnection() | One cheap GET /payments call, returns a boolean |
isConfigured(), isTestMode() | Reads the saved settings |
Every request carries the raw secret key in Authorization with no bearer prefix, a 30 second
timeout, and an Idempotency-Key on writes. It retries twice on a network error, a 429 or a
5xx. Failures throw a typed exception: AuthenticationException, PermissionException,
NotFoundException, RateLimitException, InvalidRequestException or ApiConnectionException,
all extending ApiException with readonly errorCode, httpStatus and body properties.
The REST routes
Four routes, all under wajub/v1. All of them are publicly reachable by design, and each one
carries its own proof.
| Route | Method | Guarded by |
|---|---|---|
/webhook | POST | HMAC signature and a 300 second timestamp window |
/callback | GET | Nothing, it only redirects. The status is re-read from the API |
/embed-status | GET | A nonce tied to the order id, plus the order key |
/create-payment | POST | A WordPress nonce, plus 20 requests per minute per IP |
There is also a legacy webhook path, POST /?wajub=webhook, kept for sites configured before the
REST route existed. It verifies the same signature. Use the REST route for anything new.
The transactions table
Every webhook writes a row in wp_wajub_transactions, whatever integration it belongs to. It is
the one place where a WooCommerce order, a GiveWP donation and a shortcode payment sit side by
side. Wajub, Transactions renders it.
| Column | Holds |
|---|---|
reference | Your reference, unique |
trxref | The Wajub side reference |
amount, currency | As sent by the API |
status | The last status seen |
source, source_id | The metadata pair that routed the event |
customer_email | From the payment |
environment | test or live, from the switch at the time of writing |
metadata, payload | JSON |
created_at, updated_at | Timestamps |
The dashboard totals succeeded volume grouped by currency rather than summing everything into one number, so a site taking both XAF and GHS sees two figures instead of one meaningless one.
Uninstalling the plugin drops this table and deletes every wajub_ option. Deactivating does
not. 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
Paste the live secret key and the live webhook secret, and register the same URL as a live webhook endpoint in the dashboard. Live and sandbox endpoints are separate.
- 4
Turn the test switch off
Uncheck test mode in Wajub, Settings. Confirm the dashboard reads Live and Connected.
- 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 secret key saved for the current environment | The gateway hides itself until one is. Fill the field for the environment the switch selects |
| 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. Send the signed test request above |
| Real money moved while in test mode | The sandbox key field was empty and the client fell back to the live key | Fill both fields, or clear the live one until you go live |
| 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 redirects to a separate pay page | The embed script did not load | Check the console for a blocked request to js.wajub.com. The redirect 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 |
| The admin dashboard is slow | It calls the API once per page load to show the connection state | Expected. It is one request, and only on Wajub admin screens |
| Nothing is logged | Debug logging is off | Enable it in 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.