# Implementation

Complete guide to integrating Checkout Drop-in into your application.

## Overview

Checkout Drop-in provides a complete, pre-built payment interface that automatically handles multiple payment methods. It follows a simple three-step lifecycle:

1. **Initialise:** Configure Drop-in with your session and transaction data.
2. **Mount:** Render the payment interface to your page.
3. **Handle callbacks:** Respond to payment success, errors, and other events.


Drop-in automatically detects available payment methods, renders the UI, and handles all payment flows.

Backend verification is mandatory. Always verify payments on your backend before fulfilling orders. Frontend callbacks can be manipulated by malicious users.

## Before you start

Make sure you've [activated the Checkout Drop-in service](/guides/checkout/drop-in/activation) in the Unity Portal.

## Step 1: Install the Web SDK library

Install the latest version of the Web SDK from the npm public registry. You'll need to have Node.js 22.x or higher.

```shell
npm i @pxpio/web-components-sdk
```

Checkout Drop-in is part of the main SDK package. Import it directly from `@pxpio/web-components-sdk/src/checkoutDropIn/CheckoutDropIn`.

## Step 2: Get your API credentials

In order to initialise Checkout Drop-in, you'll need to send authenticated requests to the PXP API.

To get your credentials:

1. In the Unity Portal, go to **Merchant setup > Merchant groups**.
2. Select a merchant group.
3. Click the **Inbound calls** tab.
4. Copy the *Client ID* in the top-right corner.
5. Click **New token**.
6. Choose a number of days before token expiry. For example, `30`.
7. Click **Save** to confirm. Your token is now created.
8. Copy the token ID and token value. Make sure to keep these confidential to protect the integrity of your authentication process.


As best practice, we recommend regularly generating and implementing new tokens.

## Step 3: Create a session on your backend

Checkout Drop-in requires a session from the PXP Sessions API. This must be done on your backend using HMAC authentication to keep your credentials secure.

### Understanding HMAC authentication

Our platform uses HMAC (Hash-based Message Authentication Code) with SHA256 for authentication to ensure secure communication and data integrity. This method involves creating a signature by hashing your request data with a secret key, which must then be included in the HTTP headers of your API request.

### Create an HMAC signature

To create the HMAC signature, you need to prepare a string that includes five parts separated by colons:

- **Token ID:** Your API token identifier (e.g., `9aac6071-38d0-4545-9d2f-15b936af6d7f`)
- **Timestamp:** The current time in Unix milliseconds (e.g., `1754701373`)
- **Request ID:** A unique GUID for this request (e.g., `ce244054-b372-42c2-9102-f0d976db69f6`)
- **Request path:** The API endpoint path: `api/v1/sessions`
- **Request body:** The complete JSON request body as a minified string


Example request body to minify:

```json
{
  "merchant": "MERCHANT-1",
  "site": "SITE-1",
  "sessionTimeout": 120,
  "merchantTransactionId": "0ce72cfd-014d-4256-a006-a56601b2ffc4",
  "transactionMethod": {
    "intent": {
      "card": "Authorisation",
      "paypal": "Purchase"
    }
  },
  "amounts": {
    "currencyCode": "USD",
    "transactionValue": 25.00
  },
  "allowTransaction": true,
  "serviceType": "CheckoutDropIn"
}
```

When creating the HMAC signature, the request body must be minified (no whitespace or formatting). The formatted JSON above is for readability only.

### Session request parameters

The session request accepts the following parameters:

| Parameter | Description |
|  --- | --- |
| `merchant`string (≤ 20 characters) | Your unique merchant identifier, as assigned by PXP. You can find it in the Unity Portal, by going to **Merchant setup > Merchants** and checking the *Merchant ID* column. |
| `site`string (≤ 20 characters) | Your unique site identifier, as assigned by PXP. You can find it in the Unity Portal, by going to **Merchant setup > Sites** and checking the *Site ID* column. |
| `merchantTransactionId`string (≤ 50 characters) | A unique identifier of your choice that represents this transaction. |
| `sessionTimeout`number | The duration of the session, in minutes. |
| `transactionMethod`object | Details about the transaction method, including the intent for each payment type. |
| `transactionMethod.intent`object | The transaction intent for each payment method. |
| `transactionMethod.intent.card`string | The intent for card, Apple Pay, or Google Pay transactions.Possible values:`Authorisation``Purchase``Verification``EstimatedAuthorisation` |
| `transactionMethod.intent.paypal`string | The intent for PayPal transactions.Possible values:`Authorisation``Purchase` |
| `amounts`object | Details about the transaction amount. |
| `amounts.currencyCode`string (3 characters) | The currency code associated with the transaction, in ISO 4217 format. See [Supported payment currencies](/guides/checkout/drop-in/how-it-works#supported-currencies). |
| `amounts.transactionValue`number | The transaction amount. The numbers after the decimal will be zero padded if they are less than the expected `currencyCode` exponent. For example, GBP 1.1 = GBP 1.10, USD 1 = USD 1.00, or BHD 1.3 = 1.300. The transaction will be rejected if numbers after the decimal are greater than the expected `currencyCode` exponent (e.g., GBP 1.234), or if a decimal is supplied when the `currencyCode` of the exponent does not require it (e.g., JPY 1.0). |
| `allowTransaction`boolean | Whether or not to proceed with the transaction. |


### Prepare and send your request

The HMAC signature requires combining your token ID, timestamp, request ID, request path, and request body. Here's the format:

**String to hash:** `{tokenId}:{timestamp}:{requestId}:{requestPath}:{requestBody}`

For example:

```
9aac6071-38d0-4545-9d2f-15b936af6d7f:1754701373:ce244054-b372-42c2-9102-f0d976db69f6:api/v1/sessions:{"merchant":"MERCHANT-1","site":"SITE-1","sessionTimeout":120,"merchantTransactionId":"0ce72cfd-014d-4256-a006-a56601b2ffc4","transactionMethod":{"intent":{"card":"Authorisation","paypal":"Purchase"}},"amounts":{"currencyCode":"USD","transactionValue":25.00},"allowTransaction":true,"serviceType":"CheckoutDropIn"}
```

Use your **token value** (not token ID) as the secret key to create an HMAC SHA256 hash of this string. The result will be a base64-encoded signature like:

```
1DE2DFC390D7CD746A972140F26846AFA81CF85F5A0BAABA95DBC95301795EA6
```

You can now put together your `Authorization` header. It follows this format: `{tokenId}:{timestamp}:{requestId}:{signature}`. For example:

```
9aac6071-38d0-4545-9d2f-15b936af6d7f:1754701373:ce244054-b372-42c2-9102-f0d976db69f6:1DE2DFC390D7CD746A972140F26846AFA81CF85F5A0BAABA95DBC95301795EA6
```

Lastly, send your request to the Sessions API. You'll need to add a request ID of your choice and include your client ID, which you can find in the Unity Portal.

Here's a full example of what your request might look like:

```curl
curl -i -X POST \
  'https://api-services.pxp.io/api/v1/sessions' \
  -H 'Authorization: 9aac6071-38d0-4545-9d2f-15b936af6d7f:1754701373:ce244054-b372-42c2-9102-f0d976db69f6:1DE2DFC390D7CD746A972140F26846AFA81CF85F5A0BAABA95DBC95301795EA6' \
  -H 'X-Request-Id: ce244054-b372-42c2-9102-f0d976db69f6' \
  -H 'X-Client-Id: f47ac10b-58cc-4372-a567-0e02b2c3d479' \
  -H 'Content-Type: application/json' \
  -d '{
  "merchant": "MERCHANT-1",
  "site": "SITE-1",
  "sessionTimeout": 120,
  "merchantTransactionId": "0ce72cfd-014d-4256-a006-a56601b2ffc4",
  "transactionMethod": {
    "intent": {
      "card": "Authorisation",
      "paypal": "Purchase"
    }
  },
  "amounts": {
    "currencyCode": "USD",
    "transactionValue": 25.00
  },
  "allowTransaction": true,
  "serviceType": "CheckoutDropIn"
}'
```

### Session response

If your request is successful, you'll receive a `200` response containing the session data:

```json
{
  "sessionId": "c5f0799b-0839-43ce-abc5-5b462a98f250",
  "hmacKey": "904bc42395d4af634e2fd48ee8c2c7f52955a1da97a3aa3d82957ff12980a7bb",
  "encryptionKey": "20d175a669ad3f8c195c9c283fc86155",
  "sessionExpiry": "2025-05-19T13:39:20.3843454Z",
  "allowedFundingTypes": {
    "cardSchemes": [
      "Visa",
      "Diners",
      "Mastercard",
      "AmericanExpress"
    ],
    "cards": [],
    "wallets": {
      "paypal": {
        "allowedFundingOptions": [
          "venmo", 
          "paylater", 
          "paypal"
        ],
        "merchantId": "paypal-merchant-123"
      },
      "googlePay": {
        "merchantId": "BCR2DN4TWWPKJ45P",
        "merchantName": "Your Store Name",
        "gatewayMerchantId": "gateway-merchant-id"
      },
      "applePay": {
        "merchantId": "merchant.com.yourstore"
      }
    }
  }
}
```

Checkout Drop-in automatically detects available payment methods from the `allowedFundingTypes` in your session data. You don't need to manually configure which payment methods to show!

## Step 4: Initialise Drop-in on your frontend

Import `CheckoutDropIn` and `IntentType` from the SDK and initialise with your configuration.

```typescript
import CheckoutDropIn from '@pxpio/web-components-sdk/src/checkoutDropIn/CheckoutDropIn';
import IntentType from '@pxpio/web-components-sdk/src/basePxpCheckout/types/IntentType';
import PaymentMethod from '@pxpio/web-components-sdk/src/components/checkoutDropInComponents/types/PaymentMethod';
import { BaseSubmitResult } from '@pxpio/web-components-sdk/src/checkoutDropIn/types/BaseSubmitResult';
import BaseSdkException from '@pxpio/web-components-sdk/src/types/sdkExceptions/BaseSdkException';

// Get session data from your backend
const sessionData = await fetch('/api/create-session', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' }
}).then(response => response.json());

// Initialise Checkout Drop-in
const checkoutDropIn = CheckoutDropIn.initialize({
  environment: 'test',
  session: sessionData,
  ownerId: 'MERCHANT-1',
  ownerType: 'MerchantGroup',
  transactionData: {
    currency: 'USD',
    amount: 25.00,
    entryType: 'Ecom',
    intent: {
      card: IntentType.Authorisation,
      paypal: IntentType.Authorisation
    },
    merchantTransactionId: crypto.randomUUID(),
    merchantTransactionDate: () => new Date().toISOString()
  },
  onGetShopper: () => Promise.resolve({ id: 'shopper-123' }),
  onBeforeSubmit: async (paymentMethod: PaymentMethod) => {
    console.log('Payment method selected:', paymentMethod);
    return true; // Return true to proceed, false to cancel
  },
  onSubmit: (paymentMethod: PaymentMethod) => {
    console.log('Payment being processed:', paymentMethod);
  },
  onSuccess: async (result: BaseSubmitResult) => {
    // CRITICAL: Verify on backend before fulfilling order
    await verifyPaymentOnBackend(result);
  },
  onError: (error: BaseSdkException) => {
    console.error('Payment failed:', error);
    alert(`Payment failed: ${error.message}`);
  }
});
```

### Configuration parameters

The `initialize()` method accepts the following configuration parameters:

| Parameter | Description |
|  --- | --- |
| `environment`string | The environment type.Possible values:`test`: For development and testing.`production`: For live transactions. |
| `session`SessionData | Details about the checkout session returned from the Unity Sessions API. Includes `sessionId`, `hmacKey`, and `allowedFundingTypes`. |
| `ownerId`string (≤ 20 characters) | Your unique merchant or merchant group identifier, as assigned by PXP. You can find it in the Unity Portal. |
| `ownerType`string | Always set to `'MerchantGroup'` for Drop-in. |
| `transactionData`object | Details about the transaction. |
| `transactionData.currency`string (3 characters) | The currency code associated with the transaction, in ISO 4217 format. See [Supported payment currencies](/guides/checkout/drop-in/how-it-works#supported-currencies). |
| `transactionData.amount`number | The transaction amount. Must match the currency's expected decimal places. |
| `transactionData.entryType`string | The entry type.Possible values:`Ecom`: E-commerce transactions.`Moto`: Mail order/telephone order transactions. |
| `transactionData.intent`object | The transaction intents for each payment method. See [supported transaction intents](/guides/checkout/drop-in/how-it-works#supported-transaction-intents) for details. |
| `transactionData.intent.card`string | The intent for card, Google Pay, or Apple Pay transactions.Possible values:`Authorisation``EstimatedAuthorisation``Purchase``Verification` |
| `transactionData.intent.paypal`string | The intent for PayPal transactions.Possible values:`Authorisation``Purchase` |
| `transactionData.merchantTransactionId`string (≤ 50 characters) | A unique identifier for this transaction. Use a UUID or order ID. |
| `transactionData.merchantTransactionDate`function | A function that returns the current date and time in ISO 8601 format. |
| `onGetShopper`function | Function to retrieve shopper information. Required for Card-on-File functionality. Returns a Promise with shopper object containing `id`. |
| `onBeforeSubmit`function | Callback fired when a payment method is selected and the user is about to submit payment. Receives payment method. Return `true` to proceed or `false` to cancel. |
| `onSubmit`function | Callback fired when payment processing begins. Use this to show loading indicators. |
| `onSuccess`function | Callback fired when payment succeeds. |
| `onError`function | Callback fired when payment fails. Receives error details including code and message. |


## Step 5: Mount Drop-in to your page

Add a container element to your page where Drop-in will be rendered:

```html
<div id="checkout-drop-in-container"></div>
```

Then call the `create()` method to render Drop-in:

```typescript
checkoutDropIn.create('checkout-drop-in-container');
```

At this point, Drop-in will:

- Detect available payment methods from your session.
- Render a vertical accordion with all available payment methods.
- Apply branding from the Unity Portal.
- Show a "Secured by PXP" branded footer.


## Step 6: Handle callbacks

Drop-in requires three callbacks to handle payment lifecycle events.

### onGetShopper callback

Returns shopper information for Card-on-File functionality.

```typescript
onGetShopper: () => Promise.resolve({ id: 'shopper-123' })
```

### onSuccess callback

Fires when payment succeeds. **CRITICAL: Always verify on your backend before fulfilling orders.**

```typescript
onSuccess: async (result: BaseSubmitResult) => {
  // Verify payment on backend
  const verified = await fetch('/api/verify-payment', {
    method: 'POST',
    body: JSON.stringify({
      systemTransactionId: result.systemTransactionId,
      merchantTransactionId: result.merchantTransactionId
    })
  }).then(r => r.json());
  
  if (verified.success) {
    window.location.href = `/success?orderId=${verified.orderId}`;
  }
}
```

### onError callback

Fires when payment fails. Display appropriate error messages.

```typescript
onError: (error: BaseSdkException) => {
  console.error('Payment failed:', error.code, error.message);
  alert(error.message || 'Payment failed. Please try again.');
}
```

Never trust frontend callbacks for order fulfillment. Always verify payments on your backend using webhooks or the Query Transaction API before fulfilling orders.

Want to add validation before payment or show loading states? See the [Events guide](/guides/checkout/drop-in/web/events) for optional callbacks like `onBeforeSubmit` and `onSubmit`.

## Step 7: Backend verification (CRITICAL)

Frontend callbacks can be manipulated by malicious users. You must verify all payments on your backend before fulfilling orders.

### Primary method: Webhooks

Unity sends real-time webhook notifications to your backend when transactions complete:

```javascript
// Your webhook endpoint (configured in the Unity Portal)
app.post('/webhooks/pxp', async (req, res) => {
  const events = req.body;
  
  // Verify webhook authenticity using HMAC
  if (!verifyWebhookSignature(req)) {
    return res.status(401).json({ error: 'Unauthorised' });
  }
  
  for (const event of events) {
    if (event.eventCategory === 'Transaction') {
      const txn = event.eventData;
      
      // Verify transaction details
      if (txn.state === 'Authorised' || txn.state === 'Captured') {
        // Check idempotency (prevent duplicate processing)
        if (!await hasProcessedTransaction(txn.systemTransactionId)) {
          // Verify amount and merchant transaction ID
          const transactionAmount = txn.amounts?.transactionValue || txn.amount;
          if (transactionAmount === expectedAmount &&
              txn.merchantTransactionId === expectedMerchantTxnId) {
            // Payment verified - fulfill order
            await fulfillOrder(txn.merchantTransactionId);
            await markTransactionProcessed(txn.systemTransactionId);
          }
        }
      }
    }
  }
  
  // Always return success response
  res.json({ state: 'Success' });
});
```

### Optional fallback: Query the Transactions API

If you need instant feedback before the webhook arrives, you can query PXP's Transactions API:

```javascript
// Your verification endpoint
app.post('/api/verify-payment', async (req, res) => {
  const { systemTransactionId, amount, merchantTransactionId } = req.body;
  
  // Check if webhook already processed this
  const existingTxn = await getTransactionFromDB(systemTransactionId);
  if (existingTxn) {
    return res.json({ success: true, orderId: existingTxn.orderId });
  }
  
  // Query PXP API as fallback (requires HMAC authentication)
  const response = await fetch(
    `https://api-services.pxp.io/api/v1/transactions?systemTransactionId=${systemTransactionId}&fundingType=Card`,
    {
      headers: {
        'X-Client-Id': process.env.PXP_CLIENT_ID,
        'X-Request-Id': crypto.randomUUID(),
        'Authorization': createAuthHeader('/api/v1/transactions', '', process.env.PXP_TOKEN_ID, process.env.PXP_TOKEN_VALUE),
        'Content-Type': 'application/json'
      }
    }
  );
  
  const transaction = await response.json();
  
  // Verify transaction details
  const transactionAmount = transaction.amounts?.transactionValue || transaction.amount;
  if (transaction.state === 'Authorised' &&
      Math.abs(transactionAmount - amount) < 0.01 &&
      transaction.merchantTransactionId === merchantTransactionId) {
    // Payment verified - fulfill order
    const order = await fulfillOrder(merchantTransactionId);
    return res.json({ success: true, orderId: order.id });
  }
  
  res.json({ success: false, error: 'Payment verification failed' });
});
```

## Error handling

All errors return a consistent structure with `ErrorCode` and `message` properties. Common scenarios include card declined, insufficient funds, expired card, CVV failure, and 3DS authentication failure. Display the error message to users and allow them to retry or use a different payment method.

## Complete example

Here's a complete example showing Drop-in integration in a React component:

```typescript
import { useEffect, useState } from 'react';
import CheckoutDropIn from '@pxpio/web-components-sdk/src/checkoutDropIn/CheckoutDropIn';
import IntentType from '@pxpio/web-components-sdk/src/basePxpCheckout/types/IntentType';
import PaymentMethod from '@pxpio/web-components-sdk/src/components/checkoutDropInComponents/types/PaymentMethod';
import { BaseSubmitResult } from '@pxpio/web-components-sdk/src/checkoutDropIn/types/BaseSubmitResult';
import BaseSdkException from '@pxpio/web-components-sdk/src/types/sdkExceptions/BaseSdkException';

export default function CheckoutPage() {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    initializeCheckout();
  }, []);

  async function initializeCheckout() {
    try {
      // 1. Get session from backend
      const sessionData = await fetch('/api/create-session', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          merchant: "MERCHANT-1",
          site: "SITE-1",
          sessionTimeout: 120,
          merchantTransactionId: crypto.randomUUID(),
          transactionMethod: {
            intent: {
              card: "Authorisation",
              paypal: "Authorisation"
            }
          },
          amounts: {
            currencyCode: "USD",
            transactionValue: 25.00
          },
          allowTransaction: true,
          serviceType: "CheckoutDropIn"
        })
      }).then(r => r.json());

      // 2. Initialise Drop-in
      const checkoutDropIn = CheckoutDropIn.initialize({
        environment: 'production',
        session: sessionData,
        ownerId: 'MERCHANT-1',
        ownerType: 'MerchantGroup',
        transactionData: {
          currency: 'USD',
          amount: 25.00,
          entryType: 'Ecom',
          intent: {
            card: IntentType.Authorisation,
            paypal: IntentType.Authorisation
          },
          merchantTransactionId: crypto.randomUUID(),
          merchantTransactionDate: () => new Date().toISOString()
        },
        onGetShopper: () => Promise.resolve({ id: 'shopper-123' }),
        onBeforeSubmit: async (paymentMethod: PaymentMethod) => {
          console.log('Payment method selected:', paymentMethod);
          setError(null);
          return true;
        },
        onSubmit: (paymentMethod: PaymentMethod) => {
          console.log('Processing payment...');
          setIsLoading(true);
        },
        onSuccess: async (result: BaseSubmitResult) => {
          console.log('Payment successful:', result.systemTransactionId);
          
          // Verify on backend
          try {
            const verified = await fetch('/api/verify-payment', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({
                systemTransactionId: result.systemTransactionId,
                merchantTransactionId: result.merchantTransactionId
              })
            }).then(r => r.json());
            
            if (verified.success) {
              window.location.href = `/success?orderId=${verified.orderId}`;
            } else {
              setError('Payment verification failed. Please contact support.');
              setIsLoading(false);
            }
          } catch (err) {
            setError('Failed to verify payment. Please contact support.');
            setIsLoading(false);
          }
        },
        onError: (error: BaseSdkException) => {
          console.error('Payment failed:', error);
          
          // Handle errors based on message content
          let userMessage = error.message || 'Payment failed. Please try again.';
          
          if (error.message.includes('declined')) {
            userMessage = 'Your card was declined. Please try a different card.';
          } else if (error.message.includes('insufficient')) {
            userMessage = 'Insufficient funds. Please use a different payment method.';
          } else if (error.message.includes('expired')) {
            userMessage = 'This card has expired. Please use a different card.';
          } else if (error.message.includes('CVV') || error.message.includes('security')) {
            userMessage = 'Invalid security code. Please check your CVV.';
          } else if (error.code === 'SDK1114' || error.message.includes('authentication')) {
            userMessage = '3D Secure authentication failed. Please try again.';
          }
          
          setError(userMessage);
          setIsLoading(false);
        }
      });

      // 3. Mount Drop-in
      checkoutDropIn.create('checkout-drop-in-container');
      
    } catch (err) {
      console.error('Failed to initialise checkout:', err);
      setError('Failed to load payment form. Please refresh the page.');
    }
  }

  return (
    <div className="checkout-page">
      <h1>Complete Your Purchase</h1>
      
      <div className="order-summary">
        <h2>Order Summary</h2>
        <p>Product: Premium Subscription</p>
        <p>Amount: $25.00 USD</p>
      </div>
      
      {error && (
        <div className="error-message" role="alert">
          {error}
        </div>
      )}
      
      {isLoading && (
        <div className="loading-overlay">
          Processing payment...
        </div>
      )}
      
      <div id="checkout-drop-in-container"></div>
      
      <p className="security-notice">
        Your payment information is securely processed by PXP. 
        We never store your full card details.
      </p>
    </div>
  );
}
```

## What's next?

Now that you've integrated Checkout Drop-in, here are some recommended next steps:

- **[Configuration](/guides/checkout/drop-in/web/configuration):** Customise branding, colours, and styling in the Unity Portal.
- **[Events](/guides/checkout/drop-in/web/events):** Learn about all available callbacks and event handling.
- **[Testing](/guides/checkout/drop-in/web/testing):** Use test cards and sandbox environment to test your integration.
- **[Configure webhooks](/guides/get-started/about-webhooks):** Set up server-side webhook handling for reliable payment verification.