Integrate with React
Where the secret key lives in a React or Next.js app, and why it never moves.
Every React and Next.js integration, in every mode, has the same shape. Once you see it, the differences between redirect, embedded and custom fields stop being architectural and become a rendering detail.
This page is that shape. The finished code for each mode lives on React / Next.js, and a full tutorial with a real form and return page is the Next.js App Router recipe.
The rule everything follows
The secret key never leaves the server. Not in a prop, not in a bundled constant, not in a client component that "only runs at build time".
This is not advice. Wajub enforces it at the edge.
A secret key sent from a browser is refused and reported
If a request carries an sk. key together with an Origin or Referer header, the API answers
403 with a message telling you to use a public key, and queues a security alert to the email on
the key. You do not get a quiet failure and a leaked key. You get a loud failure and a warning that
the key is exposed.
So the browser never holds the key, and never needs to. Here is what it holds instead.
The four steps, unchanged across every mode
Read the last row twice. The client telling you it succeeded is a claim; the webhook is the fact.
Fulfilment hangs off the webhook, never off an onSuccess callback.
What that looks like in Next.js
A Server Action is the shortest correct version. The 'use server' directive is the boundary: the
function body is never bundled for the browser, so the key cannot reach it.
'use server';
import { Wajub } from '@wajub/node';
const wajub = new Wajub({ apiKey: process.env.WAJUB_SECRET_KEY! });
export async function startCheckout(orderId: string) {
const order = await db.orders.find(orderId);
const payment = await wajub.payments.create(
{
amount: order.amount,
currency: order.currency,
customer: { email: order.email },
reference: order.reference,
callback: `${process.env.APP_URL}/orders/${order.id}/return`,
},
{ idempotencyKey: order.reference },
);
await db.orders.update(order.id, { payment_id: payment.id });
return { url: payment.authorization_url };
}Notice that the action takes an order id and nothing else. The amount is read from your database, not from the argument. A client that can name its own price is a client that will.
The component calling it is an ordinary client component, and it never imports the SDK.
'use client';
import { useTransition } from 'react';
import { startCheckout } from '@/app/actions/checkout';
export function PayButton({ orderId }: { orderId: string }) {
const [pending, start] = useTransition();
return (
<button
disabled={pending}
onClick={() =>
start(async () => {
const { url } = await startCheckout(orderId);
window.location.href = url;
})
}
>
{pending ? 'Starting…' : 'Pay'}
</button>
);
}If you are on the Pages Router or plain React with your own backend, replace the Server Action with
a POST route. Nothing else in the shape changes.
The environment variable trap
Next.js decides what ships to the browser by prefix, and the prefix is easy to add by reflex.
| Variable | Where it ends up |
|---|---|
WAJUB_SECRET_KEY | Server only. Correct |
NEXT_PUBLIC_WAJUB_SECRET_KEY | Inlined into the JavaScript bundle. Never do this |
NEXT_PUBLIC_WAJUB_PUBLIC_KEY | Fine, that is what a public key is for |
`NEXT_PUBLIC_` is not a naming convention
It is an instruction to the bundler. Any variable carrying that prefix is substituted into the client bundle at build time and is readable by anyone who opens the page. Wajub secret keys are always plain server variables.
If you think a key may already have leaked, revoke it from Settings › Developer › API Keys. Rotation is instant and leaves your other keys alone.
What actually differs between the modes
The server half above is identical in all three. Only the last line changes: what the client does with what came back.
| Mode | Your server returns | Your client does |
|---|---|---|
| Redirect | authorization_url | window.location.href = url |
| Embedded | authorization_token | Renders <CheckoutEmbed sessionId={...} /> with it |
| Custom fields | authorization_token | Renders <PaymentComponent /> and confirms with useConfirmPayment |
Redirect is the one to start with. Wajub owns the operator list, the prompt wording and every failure screen, which is a lot of interface you do not have to build or maintain.
A session token is not a smaller secret key
authorization_token only exists to let the browser finish one specific payment. It cannot list,
refund or read anything else, which is exactly why it is safe to send to a client.
Sessions & security has the full boundary.
Then go get the code
This page deliberately stops before the component internals, because they are documented once, properly, elsewhere.
Related pages
- React / Next.js, three modesThe canonical page: redirect, embedded and custom fields, with full code.
- Next.js App Router recipeA finished tutorial: form, server action, return page, webhook.
- Sessions & securityWhat a session token can and cannot do.
- Accept a paymentThe order lifecycle behind the button.
- API authenticationKey types, prefixes, and what each one is allowed to call.