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 expired, used or unknown | Wire onLoadError. It carries session_expired, session_used, session_not_found, invalid_session or session_terminal_expired |
Embed misconfigured | 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' |
A thrown Error at call time | No sessionId, or the selector matched nothing | Both throw synchronously, before any request |
| The iframe never appears | load_error on onLoadError | The checkout origin is unreachable. Check the network tab and any content 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
A Wajub key is a prefix, a dot, then 96 characters. pk. and sk. are live, pk_test. and
sk_test. are sandbox.
| 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 | Raise timeout, or pass 0 to wait indefinitely |
getValue() returns null | Nothing has been typed yet | It fills on the first change. Read it in the handler |
| OTP never appears | Payment fields do not carry OTP | Use hosted checkout |
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 32 property whitelist |
A url() background is ignored | Values containing url(, @import, javascript:, < or > are refused |
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 |
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 support@wajub.com with the transaction reference, your merchant
domain, and the code from onLoadError or onError. Never send a key.