Components troubleshooting
The symptom, the cause underneath it, and the line that fixes it.
Almost every Wajub Components problem is one of four things: a token that is not valid any more, a page that loaded the SDK on the server, a callback nobody wired, or a config key the SDK quietly dropped. Start here before reading the code.
Nothing renders
| Symptom | Cause | Fix |
|---|---|---|
| An empty container, no error | The token is unknown, or its payment ended and it has stopped | Wire onLoadError. It carries session_not_found or session_terminal_expired. From Wajub.js 1.6.0 the checkout leaves the page right after it, without onClose |
Embed configuration error | A checkout URL pasted into an iframe by hand | Call mount(), open() or CheckoutEmbed |
| Nothing happens, no network request | mount() ran on the server | Import from @wajub/js/pure, or mark the file 'use client' |
An Error at call time | No sessionId, or the selector matched nothing | Before any request. window.wajub throws it synchronously; the @wajub/js helpers (mount(), open()…) are async and return a rejected promise instead, so await them in a try |
| The iframe never appears | load_error on onLoadError | The checkout origin is unreachable. Check the network tab and any content policy |
| A blank frame on one site, fine on another, no callback at all | Settings → Checkout embedding does not list this page's origin | Add https:// plus the domain, port included if any. Where the checkout may be embedded |
| The console reports a Content Security Policy violation | Your page's policy does not allow Wajub's hosts | Allow the hosts listed under Content Security Policy |
onLoadError is the callback that explains an empty embed
onError is for payments that fail. onLoadError is for checkouts that never start, which is
the case in most of the rows above. Wire it first, on every integration.
The keys
Live keys start with pk. or sk., sandbox keys with pk_test. or sk_test..
| Symptom | Cause | Fix |
|---|---|---|
secret_key_in_browser | An sk. key passed to Wajub() | The browser gets a session token, never a secret key |
invalid_publishable_key | The value is not a publishable key | It starts with pk, and it is not the session token |
missing_publishable_key | createPayment() with no key | Set it on the client, or create the session on your server |
403 from the API, and an email | A secret key sent from a browser | The API refuses it and alerts the key's owner. Rotate the key |
CORS on createPayment | A direct API call from page code | Create the session on your backend |
To tell sandbox from live in the browser, read the session rather than the key.
import { fetchSession } from '@wajub/js';
const preview = await fetchSession(sessionId);
if (preview.environment === 'sandbox') showSandboxBanner();Payment fields
| Symptom | Cause | Fix |
|---|---|---|
complete never becomes true | A required field is still empty or invalid | Log event.error in the change handler, it names the field |
confirmPayment rejects with wallet_not_supported | A wallet component | Apple Pay and Google Pay pay through their own button |
confirmPayment rejects with address_not_payment | An address component | It collects, it does not charge. Read it with getValue() |
confirmPayment rejects with missing_component | No component passed, or one that never mounted | Keep the instance from onInstance or the ready event |
confirmation_timeout after a minute | No outcome came back in time. The payment goes on and can still succeed | Raise timeout, or pass 0 to wait indefinitely. Before offering to pay again, listen for success or wait for the webhook |
confirmPayment rejects with checkout_validation | onBeforeSubmit returned false, threw or took more than 30 seconds | Answer within 30 seconds, and return false only when the order was refused |
confirmPayment rejects with payment_error, "Validate the order before confirming this payment." | The payment was prepared with deferred: true and never activated | Activate it from your order endpoint before onBeforeSubmit resolves true |
getValue() returns null | Nothing has been typed yet | It fills on the first change. Read it in the handler |
Styling that does nothing
The SDK filters appearance before it leaves the browser. An unknown key is dropped in silence,
so a typo looks exactly like a bug in the checkout.
| Symptom | Cause |
|---|---|
A rules block is ignored | The selector is not .Input or .Label, with at most one allowed pseudo |
| One property inside a rule is ignored | It is outside the 31 property whitelist |
| A background image or a font import is ignored | Some values are refused for security, among them any value that would load an external resource |
A variables key is ignored | Only the sixteen documented keys pass |
update({ layout }) changes nothing | On the hosted checkout it is stored locally and never sent |
appearance props change nothing after mount | CheckoutEmbed never calls update() itself |
The three lists are on Appearance, and the framework specific behaviour is on React, Vue and Svelte.
Layout and sizing
| Symptom | Cause | Fix |
|---|---|---|
| The embed is clipped | A parent with overflow: hidden or a fixed height | The SDK sets the iframe height itself. Let the parent grow |
| The page jumps when the checkout loads | Nothing is reserved around your own content | The SDK already reserves 480px. Reserve your own summary, not the embed |
| A field component stays tiny | Its parent is display: none at mount | Mount it once it is visible, or after the tab opens |
onResize is informational. The SDK has already applied the height by the time it fires, so use
it to move something else on the page.
The payment itself
| Symptom | Cause | Fix |
|---|---|---|
| A method is missing from the checkout | The session does not carry that channel | Read fetchSession().payment_methods before mounting |
| The order shipped twice | onSuccess and the webhook both fulfil | Fulfil on the webhook, use onSuccess for the interface |
| The order never shipped | onSuccess ran and the tab closed | Same answer. The browser is not a reliable reporter |
onError fires with a decline_code | The provider refused the payment | retryable says whether another attempt is worth offering |
onError fires with nothing_to_submit | submit() found no pay button to press: no method chosen, an OTP step, a wallet's own button, or a payment already on its way | Call submit() once the payer has chosen a method, and not while a payment is under way |
Before going live
onErrorandonLoadErrorare both wired.- The session is created on your server, with the secret key in an environment variable.
- Your site and your
callbackURL are HTTPS. - Fulfilment happens on the webhook, not in the browser.
- The integration goes through
mount(),open()or a framework component, never a hand built URL. onBreakdownkeeps your own total in step, if you show one beside an inline embed.- One real payment for a small amount has gone through in live mode.
Test cards and Mobile Money numbers are on Test scenarios.
Still stuck
Write to [email protected] with the transaction reference, your merchant
domain, and the code from onLoadError or onError. Never send a key.