Skip to content

Quickstart

Follow our walkthrough to get Components for Web running in minutes.

This quickstart focuses on card payments in the test environment. Components for Web also supports Apple Pay, Google Pay, PayPal, Paze, and Aeropay. See What's next? for the available payment-method guides.

Pre-requisites

Before you start, make sure you have:

  • Node.js 22.x or higher installed on your computer
  • Your API credentials from the Unity Portal

Install the SDK

Install the version of the Web SDK tested with this quickstart from the npm public registry. You'll need Node.js 22.x or higher.

npm i @pxpio/web-components-sdk

Create a session on your backend

Components for Web needs a session from the PXP API. This must happen on your backend using Hash-based Message Authentication Code (HMAC) authentication.

Store your credentials securely

Set your token ID, token value, and client ID as the PXP_TOKEN_ID, PXP_TOKEN_VALUE, and PXP_CLIENT_ID environment variables. Never hardcode them in your application.

Create the HMAC signature function

This function generates a secure authentication hash by combining timestamp, request ID, request path, and request body, then hashing with your token value using HMAC SHA256.

Build the session request body

Load the order by its ID on your backend, then create a request with the server-controlled merchant, site, amount, and currency. The request body must be minified (no whitespace) for the HMAC signature.

Generate the HMAC signature

Call the signature function with your credentials and request details.

Send the session creation request

POST to https://api-services.dev.pxp.io/api/v1/sessions with your authentication headers and request body.

Return the session data to your frontend

The API returns sessionId, hmacKey, encryptionKey, and allowedFundingTypes. Return a payload that matches what your frontend expects: the session object plus the server-controlled merchantTransactionId, amount, and currency.

Set the authentication decision

Call the Modify session API to set authentication = false. This creates a non-3D Secure (3DS) flow. See 3D Secure before enabling authentication.

Register your backend session route

Register /api/sessions on your existing backend. Require an order ID and idempotency key, then load the authoritative order before creating or reusing a session.

import crypto from 'node:crypto';

// Store your API credentials from the Unity Portal as environment variables.
const TOKEN_ID = process.env.PXP_TOKEN_ID;
const TOKEN_VALUE = process.env.PXP_TOKEN_VALUE;
const CLIENT_ID = process.env.PXP_CLIENT_ID;

if (!TOKEN_ID || !TOKEN_VALUE || !CLIENT_ID) {
  throw new Error(
    'Set PXP_TOKEN_ID, PXP_TOKEN_VALUE, and PXP_CLIENT_ID before starting the server.',
  );
}

const REQUEST_PATH = 'api/v1/sessions';

/**
 * Creates an HMAC signature for authenticating API requests
 */
function createHmacSignature(
  timestamp,
  requestId,
  requestPath,
  requestBody,
  tokenValue,
) {
  const stringToHash = `${timestamp}${requestId}${requestPath}${requestBody}`;
  const hmac = crypto.createHmac('sha256', tokenValue);

  hmac.update(stringToHash);

  return hmac.digest('hex').toUpperCase();
}

/**
 * This function is a helper to create a new checkout session from merchant backend.
 */
export async function createSession({
  merchantId,
  siteId,
  amount,
  currency,
}) {
  // Generate unique IDs for this request
  const timestamp = Math.floor(Date.now() / 1000);
  const requestId = crypto.randomUUID();
  const merchantTransactionId = crypto.randomUUID();

  // Build the request body
  const requestBody = {
    merchant: merchantId,
    site: siteId,
    sessionTimeout: 120,
    merchantTransactionId,
    transactionMethod: {
      intent: {
        card: 'Authorisation',
      },
    },
    amounts: {
      currencyCode: currency,
      transactionValue: amount,
    },
    allowTransaction: true,
  };

  const requestBodyString = JSON.stringify(requestBody);
  
  const signature = createHmacSignature(
    timestamp,
    requestId,
    REQUEST_PATH,
    requestBodyString,
    TOKEN_VALUE,
  );

  const response = await fetch(`https://api-services.dev.pxp.io/${REQUEST_PATH}`, {
    method: 'POST',
    headers: {
      'Authorization': `PXP-UST1 ${TOKEN_ID}:${timestamp}:${signature}`,
      'X-Request-Id': requestId,
      'X-Client-Id': CLIENT_ID,
      'Content-Type': 'application/json',
    },
    body: requestBodyString,
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(
      `Session creation failed (${response.status}): ${errorText || response.statusText}`,
    );
  }

  const sessionData = await response.json();

  return {
    session: sessionData,
    transactionData: {
      merchantTransactionId,
      amount,
      currency,
    },
  };
}

Initialise the card component on your frontend

Import the required dependencies

Import PxpCheckout and necessary types from the Web SDK.

Set up your React component

Create a React component that hosts the card payment interface. Pass the authenticated order ID and merchant group ID from your application.

Fetch the session data from your backend

Call your backend endpoint to get the session data you created in the previous steps.

Initialise the SDK

Configure the SDK with your environment, session data, owner details, and transaction information.

Configure the transaction data

Specify the currency, amount, entry type, and payment intent for card transactions.

Provide shopper information

Implement onGetShopper to get shopper details for the authenticated customer from your backend. The SDK calls this callback whenever it needs shopper data during the payment flow.

The shopper id enables card-on-file (COF) and one-click payments. It also identifies the shopper when they consent to storing a card. Use a stable, unique ID from your authenticated backend. Don't use a shared or browser-supplied value.

If you don't provide a shopper ID:

  • Card-on-file components can't retrieve saved cards.
  • One-click payment functionality doesn't work.
  • The consent tickbox can't store a card against a shopper.

If you provide a shopper ID:

  • Card-on-file and one-click components retrieve cards from the Token Vault for that shopper.
  • When the shopper selects the configured consent checkbox, their card is stored under that shopper ID for future use.

Create the new card component

Use the SDK's create() method to create a new card component with your desired configuration.

Configure the card input fields

Set up the card number, expiry date, Card Verification Code (CVC), cardholder name, and card-consent fields.

Configure the submit button

Customise the submit button text and styling to match your brand.

Handle successful payments

Implement onPostAuthorisation to handle the PXP response. The callback can receive either MerchantSubmitResult or FailedSubmitResult. Always verify successful payments on your backend before fulfilling orders because frontend callbacks can be manipulated.

Handle payment errors

Implement onSubmitError to handle SDK, network, or unexpected submission errors. Failed PXP transaction responses are returned to onPostAuthorisation as FailedSubmitResult.

Implement pre-authorisation callback

Implement the asynchronous onPreAuthorisation callback. Return an empty object {} to proceed, or include Address Verification Service (AVS) or risk-screening data.

Mount the component to the page

Call the mount() method with your container element ID to render the payment form.

Add a container element to your page

Return JSX that includes a container div where the card component will mount itself.

import {
  FailedSubmitResult,
  IntentType,
  MerchantSubmitResult,
  NewCardComponent,
  PxpCheckout,
  type NewCardComponentConfig,
  type SessionData,
} from '@pxpio/web-components-sdk';
import { useEffect, useRef, useState } from 'react';

type CheckoutSessionResponse = {
  session: SessionData;
  transactionData: {
    merchantTransactionId: string;
    amount: number;
    currency: string;
  };
};

export default function CheckoutPage({
  orderId,
  merchantGroupId,
}: {
  orderId: string;
  merchantGroupId: string;
}) {
  const [sessionData, setSessionData] = useState<CheckoutSessionResponse | null>(null);
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
  const sessionRequestId = useRef(crypto.randomUUID());

  useEffect(() => {
    const controller = new AbortController();

    async function fetchSession() {
      const response = await fetch('/api/sessions', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Idempotency-Key': sessionRequestId.current,
        },
        body: JSON.stringify({ orderId }),
        signal: controller.signal,
      });

      if (!response.ok) {
        throw new Error(`Session creation failed with status ${response.status}.`);
      }

      setSessionData(await response.json() as CheckoutSessionResponse);
    }

    fetchSession().catch((error: unknown) => {
      if (error instanceof DOMException && error.name === 'AbortError') {
        return;
      }

      console.error('Unable to create checkout session:', error);
      setErrorMessage('Unable to start checkout. Try again.');
    });

    return () => controller.abort();
  }, [orderId]);

  useEffect(() => {
    if (!sessionData) {
      return;
    }

    const pxpSdk = PxpCheckout.initialize({
      environment: 'test',
      session: sessionData.session,
      ownerId: merchantGroupId,
      ownerType: 'MerchantGroup',
      transactionData: {
        currency: sessionData.transactionData.currency,
        amount: sessionData.transactionData.amount,
        entryType: 'Ecom',
        intent: {
          card: IntentType.Authorisation,
        },
        merchantTransactionId: sessionData.transactionData.merchantTransactionId,
        merchantTransactionDate: () => new Date().toISOString(),
      },
      onGetShopper: async () => {
        const response = await fetch('/api/shopper');
        if (!response.ok) {
          throw new Error(`Shopper retrieval failed with status ${response.status}.`);
        }

        return response.json();
      },
    });

    const config: NewCardComponentConfig = {
      fields: {
        cardNumber: {
          required: true,
          placeholder: '1234 5678 9012 3456',
          acceptedCardBrands: ['visa', 'mastercard', 'cup', 'diners', 'discover', 'jcb'],
        },
        expiryDate: {
          required: true,
          placeholder: 'MM/YY',
        },
        cvc: {
          required: true,
          placeholder: '123',
        },
        holderName: {
          required: true,
          placeholder: 'John Doe',
        },
        cardConsent: {
          isShow: true,
        },
      },
      submit: {
        submitText: 'Pay $25.00',
        styles: {
          base: {
            backgroundColor: '#4CAF50',
            color: 'white',
            padding: '15px',
            borderRadius: '6px',
            fontSize: '16px',
            fontWeight: 'bold',
            width: '100%',
          },
        },
        onPreAuthorisation: async () => ({}),
        onPostAuthorisation: async (data) => {
          if (data instanceof FailedSubmitResult) {
            console.error('Payment declined:', data.errorCode, data.errorReason);
            setErrorMessage('Payment failed. Try again or use another payment method.');
            return;
          }

          const result = data as MerchantSubmitResult;
          const response = await fetch('/api/verify-payment', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              orderId,
              systemTransactionId: result.systemTransactionId,
              merchantTransactionId: result.merchantTransactionId,
            }),
          });

          if (!response.ok) {
            setErrorMessage('Payment verification is unavailable. Contact support.');
            return;
          }

          const verified = await response.json() as {
            success: boolean;
            orderId?: string;
          };

          if (verified.success && verified.orderId) {
            globalThis.location.href = `/success?orderId=${verified.orderId}`;
            return;
          }

          setErrorMessage('Payment could not be verified. Contact support.');
        },
        onSubmitError: (error) => {
          console.error('Payment submission error:', error);
          setErrorMessage('Payment could not be submitted. Try again.');
        },
      },
    };

    const newCard = pxpSdk.create('new-card', config) as NewCardComponent;

    newCard.mount('new-card-container');

    return () => {
      newCard.unmount();
    };
  }, [merchantGroupId, orderId, sessionData]);

  return (
    <div>
      <h1>Complete your purchase</h1>
      {errorMessage && <p role="alert">{errorMessage}</p>}
      <div id="new-card-container"></div>
    </div>
  );
}

Verify payments

When PXP returns an authorisation response, onPostAuthorisation receives a success or failure result. For MerchantSubmitResult, always verify the payment on your backend before fulfilling orders.

Configure webhooks in the Unity Portal to receive real-time payment notifications on your backend.

Create the webhook endpoint

Register /webhooks/pxp to receive payment notifications from Unity. Verify the webhook signature against the raw request body before processing any event. Pass your Express-compatible application, signature verifier, and persistence functions to registerPxpWebhook().

Process webhook events

Loop through the events array and filter for Transaction events.

Check payment state

Verify the transaction state is Authorised or Captured before processing.

Prevent duplicate processing

Skip events you've already handled by checking systemTransactionId before fulfilment.

Verify transaction details

Match the merchantTransactionId, amount, and currency against your order records.

Fulfil the order

If verification passes, fulfil the order and mark the transaction as processed.

Respond to webhook

After authenticating the request, return { state: 'Success' } to acknowledge receipt. Handle fulfilment failures through your own retry and incident process rather than failing the webhook response in this sample.

Register the verification endpoint

Register /api/verify-payment on your backend. Load the authenticated customer's order and retrieve the authoritative transaction from PXP before returning a verified result.

You can also verify payments using the Transactions API to query transaction status directly. See Backend verification for details.

export function registerPxpWebhook(
  app,
  {
    isTransactionProcessed,
    getOrderByMerchantTransactionId,
    fulfilOrder,
    markTransactionProcessed,
  },
) {
  // Configure this webhook URL in the Unity Portal.
  // Verify the Unity webhook signature in middleware before this handler runs.
  app.post('/webhooks/pxp', async (req, res) => {
  const events = Array.isArray(req.body) ? req.body : [];
  
  // Process each webhook event
  for (const event of events) {
    if (event.eventCategory === 'Transaction') {
      const txn = event.eventData;
      
      // Check if payment was successful
      if (txn.state === 'Authorised' || txn.state === 'Captured') {
        // Prevent duplicate processing
        const alreadyProcessed = await isTransactionProcessed(txn.systemTransactionId);
        if (alreadyProcessed) {
          continue;
        }
        
        // Verify transaction details match your records
        const expectedOrder = await getOrderByMerchantTransactionId(txn.merchantTransactionId);
        
        if (expectedOrder && 
            txn.amounts.transactionValue === expectedOrder.amount &&
            txn.amounts.currencyCode === expectedOrder.currency) {
          
          // Payment verified. Fulfil the order.
          await fulfilOrder(txn.merchantTransactionId);
          
          // Mark as processed to prevent duplicates
          await markTransactionProcessed(txn.systemTransactionId);
          
          console.log(`Order ${txn.merchantTransactionId} fulfilled successfully`);
        } else {
          console.error('Transaction verification failed - amount or currency mismatch');
        }
      }
    }
  }
  
  // Acknowledge receipt after the request has been authenticated.
  res.json({ state: 'Success' });
  });
}

You now have the core fragments needed to add a card payment integration to your application.

What's next?

Now that you have card components running, here are the recommended next steps:

Explore other payment methods:

Enhance your card integration: