Skip to content

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.

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.

app/actions/checkout.ts
'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.

app/checkout/PayButton.tsx
'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.

VariableWhere it ends up
WAJUB_SECRET_KEYServer only. Correct
NEXT_PUBLIC_WAJUB_SECRET_KEYInlined into the JavaScript bundle. Never do this
NEXT_PUBLIC_WAJUB_PUBLIC_KEYFine, that is what a public key is for

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.

ModeYour server returnsYour client does
Redirectauthorization_urlwindow.location.href = url
Embeddedauthorization_tokenRenders <CheckoutEmbed sessionId={...} /> with it
Custom fieldsauthorization_tokenRenders <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.

What did you think of this content?