Skip to content

Widget Integration Guide ​

PayBridge provides an embeddable checkout widget that renders a PCI-compliant payment form using VGS Collect secure iframes. This guide covers installation, configuration, callbacks, styling, security, and troubleshooting.

Installation ​

Include the widget script and stylesheet from your PayBridge instance:

html
<!-- CSS is bundled into the JS file (injected at runtime), so no separate stylesheet is needed -->
<script src="https://your-pb-host.com/widget/pb-widget.umd.js"></script>

Or use the ES module build:

html
<script type="module">
  import { initCheckout } from "https://your-pb-host.com/widget/pb-widget.es.js";
  initCheckout(document.getElementById("pb-checkout"));
</script>

npm (if bundling) ​

bash
npm install @paybridge/widget
js
import { initCheckout } from "@paybridge/widget";

Quick Start ​

Minimal HTML to render a checkout form:

html
<div
  id="pb-checkout"
  data-merchant="550e8400-e29b-41d4-a716-446655440000"
  data-amount="5000"
  data-api-url="https://api.nfs-pay.com"
  data-widget-token="your_widget_token_here"
  data-vgs-vault-id="tntabc12345"
  data-vgs-environment="live"
  data-redirect="https://yoursite.com/thank-you"
></div>

<script>
  PayBridge.initCheckout(document.getElementById("pb-checkout"));
</script>

This renders a tabbed card/bank form with a "Pay $50.00 USD" button.

Configuration ​

All configuration is passed via data-* attributes on the container element.

AttributeTypeRequiredDefaultDescription
data-merchantstring (UUID)Yes--Merchant UUID to charge on behalf of
data-amountintegerYes--Amount in minor units (cents). 5000 = $50.00
data-currencystringNo"USD"ISO 4217 currency code (USD, EUR, GBP, CAD)
data-api-urlstring (URL)Yes"http://localhost:8000"PayBridge API base URL. Must be HTTPS on non-localhost domains
data-widget-tokenstringYes--Short-lived HMAC-signed token from POST /widget/sessions
data-redirectstring (URL)No--URL to redirect the customer to after successful payment
data-vgs-vault-idstringYes--VGS vault ID for secure iframe fields
data-vgs-environmentstringNo"sandbox"VGS environment: "sandbox" or "live"
data-on-successstringNo--Name of a window function to call on successful payment
data-on-errorstringNo--Name of a window function to call on payment error
data-api-keystringNo--Development fallback. Use data-widget-token in production

Amount Format ​

All amounts are in minor units (cents). Examples:

Displaydata-amount
$10.001000
$50.005000
$100.0010000
$999,999.9999999999

Minimum: 1 (one cent). Maximum: 99,999,999 ($999,999.99).

Widget Token Flow (Security) ​

Never embed your raw API key in client-side HTML. Instead, use the widget token exchange:

  Your Backend                  PayBridge API              Customer Browser
  -----------                   ------------------             ----------------
      |                                |                              |
      |-- POST /widget/sessions ------>|                              |
      |   (X-API-Key: your_key)        |                              |
      |   { merchant_id, actions,      |                              |
      |     ttl_seconds: 300 }         |                              |
      |<-- { widget_token, expires } --|                              |
      |                                |                              |
      |-- Render HTML with ----------->|                              |
      |   data-widget-token            |                              |
      |                                |                              |
      |                                |<-- Widget sends payment -----|
      |                                |    (X-Widget-Token header)   |
      |                                |-- Payment result ----------->|

Step 1: Your backend calls POST /widget/sessions with your API key (server-to-server):

bash
curl -X POST https://api.nfs-pay.com/widget/sessions \
  -H "X-API-Key: your_api_key" \
  -H "Content-Type: application/json" \
  -H "X-Requested-With: PayBridge" \
  -d '{
    "merchant_id": "550e8400-e29b-41d4-a716-446655440000",
    "allowed_actions": ["checkout"],
    "ttl_seconds": 300
  }'

Response:

json
{
  "widget_token": "eyJhcHBfaWQiOi...",
  "expires_at": "2026-03-13T12:05:00Z"
}

Step 2: Pass the widget_token to your frontend HTML:

html
<div id="pb-checkout"
     data-widget-token="eyJhcHBfaWQiOi..."
     data-merchant="550e8400-e29b-41d4-a716-446655440000"
     data-amount="5000"
     ...>
</div>

The widget token is:

  • HMAC-signed -- cannot be forged without the server secret
  • Scoped to a single merchant and specific actions (e.g., checkout)
  • Short-lived -- default 5 minutes, max 1 hour (configurable via ttl_seconds)

Callbacks ​

onSuccess ​

Called when payment completes successfully. Register via data-on-success:

html
<div id="pb-checkout" data-on-success="onPaymentSuccess" ...></div>

<script>
  function onPaymentSuccess(result) {
    console.log("Transaction ID:", result.transaction_id);
    console.log("Status:", result.status);  // "success"
    console.log("Amount:", result.amount);   // e.g. 5000
  }
</script>

result object:

PropertyTypeDescription
transaction_idstringPayBridge transaction UUID
statusstringAlways "success"
amountnumberCharged amount in minor units (cents)

After the callback fires, the widget shows a success message. If data-redirect is configured and passes security validation, the customer is redirected after 1.5 seconds.

onError ​

Called when payment fails. Register via data-on-error:

html
<div id="pb-checkout" data-on-error="onPaymentError" ...></div>

<script>
  function onPaymentError(error) {
    console.log("Error code:", error.error_code);
    console.log("Message:", error.message);
  }
</script>

error object:

PropertyTypeDescription
error_codestringMachine-readable code (see Error Codes below)
messagestringHuman-readable error message

onCancel ​

There is no explicit cancel callback. If the customer navigates away, the beforeunload event warns them if a payment is being processed. You can detect abandonment by checking whether the success redirect was reached.

Error Codes ​

CodeDescriptionRetryable
PAYMENT_DECLINEDProcessor declined the paymentNo
INVALID_PAYMENT_METHODCard/bank details are invalidNo
PROCESSOR_TRANSIENTTemporary processor issueYes
PROCESSOR_ERRORProcessor errorYes
VGS_ERRORSecure form failed to loadYes
VGS_VALIDATIONClient-side field validation failedYes (after fixing input)
NETWORK_ERRORNetwork connectivity issueYes
HTTP_429Rate limit exceeded (30s cooldown)Yes (after cooldown)
HTTP_401Session/token expiredNo (re-create widget token)
HTTP_408 / HTTP_504Request timeoutYes
HTTP_502 / HTTP_503Gateway/service unavailableYes
UNKNOWNUnexpected errorYes

CSS Customization ​

Class Names ​

All widget elements use the pb- prefix. Override these classes to customize appearance:

ClassElement
.pb-widgetRoot container
.pb-tabsTab bar container
.pb-tabIndividual tab button
.pb-tab.activeActive tab
.pb-form-groupForm field wrapper
.pb-labelField label
.pb-vgs-fieldVGS secure iframe container
.pb-vgs-field-errorField with validation error
.pb-errorInline error message text
.pb-rowHorizontal field row (expiry + CVC)
.pb-colColumn within a row
.pb-btnSubmit button
.pb-btn:disabledDisabled submit button
.pb-btn-successButton after successful payment
.pb-spinnerLoading spinner animation
.pb-alertAlert message container
.pb-alert-errorError alert
.pb-alert-successSuccess alert
.pb-selectNative select element (account type dropdown)
.pb-retry-btnRetry button
.pb-return-link"Go Back" link on non-retryable declines

CSS Custom Properties (Design Tokens) ​

The widget exposes its colours as --pb-* custom properties scoped to .pb-widget. Override any of them on your container element to match your brand — this is the intended customization path:

TokenDefaultControls
--pb-primary#0078D4Primary button, active tab, focus ring
--pb-primary-hover#106EBEButton hover
--pb-success#038833Success button, positive states
--pb-error#DE3131Error borders and text
--pb-surface#FFFFFFField backgrounds
--pb-background#F3F4F6Section background
--pb-border#E7E8EAField borders
--pb-text#151A24Labels and input text
--pb-text-muted#687790Placeholder and helper text
--pb-text-disabled#616161Disabled control text
css
/* Match the widget to your brand */
#pb-checkout {
  --pb-primary: #7c3aed;
  --pb-primary-hover: #6d28d9;
}

Sizing and Layout ​

The widget has a default max-width: 480px. Override on the root container:

css
.pb-widget {
  max-width: 600px;
}

Button Customization ​

css
/* Primary brand color */
.pb-btn {
  background-color: #4f46e5;  /* Indigo */
  border-radius: 8px;
  font-size: 1rem;
  height: 3rem;
}

.pb-btn:hover {
  background-color: #4338ca;
}

/* Success state */
.pb-btn-success {
  background-color: #059669;
}

Tab Customization ​

css
.pb-tab.active {
  color: #4f46e5;
  border-bottom-color: #4f46e5;
}

VGS Field Styling ​

The outer border/container is styled via .pb-vgs-field. The inner iframe content (font, text color, placeholder) is controlled by the VGS_FIELD_CSS object in the widget source. To customize inner field styles, you must modify the widget source and rebuild.

css
/* Outer container */
.pb-vgs-field {
  border: 2px solid #d1d5db;
  border-radius: 8px;
  height: 3rem;
}

.pb-vgs-field:focus-within {
  border-color: #4f46e5;
  box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
}

/* Error state */
.pb-vgs-field-error {
  border-color: #dc2626;
}

Dark Mode Example ​

css
.pb-widget {
  color: #f3f4f6;
}

.pb-label {
  color: #d1d5db;
}

.pb-vgs-field {
  background-color: #1f2937;
  border-color: #4b5563;
}

.pb-alert-error {
  background-color: #450a0a;
  color: #fca5a5;
  border-color: #7f1d1d;
}

.pb-alert-success {
  background-color: #052e16;
  color: #86efac;
  border-color: #14532d;
}

Examples ​

Card Payment with Callbacks ​

html
<!-- CSS is bundled into JS — no separate stylesheet needed -->

<div id="pb-checkout"
     data-merchant="550e8400-e29b-41d4-a716-446655440000"
     data-amount="2500"
     data-currency="USD"
     data-api-url="https://api.nfs-pay.com"
     data-widget-token="eyJhcHBfaWQiOi..."
     data-vgs-vault-id="tntabc12345"
     data-vgs-environment="live"
     data-redirect="https://yoursite.com/thank-you"
     data-on-success="handleSuccess"
     data-on-error="handleError">
</div>

<script src="https://api.nfs-pay.com/widget/pb-widget.umd.js"></script>
<script>
  function handleSuccess(result) {
    // Track conversion
    analytics.track("payment_completed", {
      transaction_id: result.transaction_id,
      amount: result.amount,
    });
  }

  function handleError(error) {
    // Log error
    analytics.track("payment_error", {
      code: error.error_code,
      message: error.message,
    });
  }

  PayBridge.initCheckout(document.getElementById("pb-checkout"));
</script>

Bank Payment (ACH) ​

The widget shows both Card and Bank Account tabs by default. The customer selects the "Bank Account" tab and fills in:

  • Account Holder Name
  • Routing Number (9 digits)
  • Account Number (10-17 digits)
  • Account Type (Checking or Savings)

No additional configuration is needed -- the same data-* attributes apply to both payment types.

Custom Styled Widget ​

html
<style>
  #payment-form {
    padding: 2rem;
    background: #fafafa;
    border-radius: 12px;
    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
  }

  #payment-form .pb-btn {
    background-color: #7c3aed;
    border-radius: 9999px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }

  #payment-form .pb-btn:hover {
    background-color: #6d28d9;
  }

  #payment-form .pb-tab.active {
    color: #7c3aed;
    border-bottom-color: #7c3aed;
  }

  #payment-form .pb-vgs-field {
    border-radius: 8px;
    border: 2px solid #e5e7eb;
  }

  #payment-form .pb-vgs-field:focus-within {
    border-color: #7c3aed;
    box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.15);
  }
</style>

<div id="payment-form"
     data-merchant="550e8400-e29b-41d4-a716-446655440000"
     data-amount="10000"
     data-api-url="https://api.nfs-pay.com"
     data-widget-token="eyJhcHBfaWQiOi..."
     data-vgs-vault-id="tntabc12345"
     data-vgs-environment="live">
</div>

<script>
  PayBridge.initCheckout(document.getElementById("payment-form"));
</script>

Security ​

HTTPS Enforcement ​

On non-localhost domains, the widget blocks initialization if data-api-url does not use HTTPS. An error message is displayed in the container:

Configuration error: API URL must use HTTPS.

Redirect URL Validation ​

The data-redirect URL is validated before navigation:

  • Same-origin URLs are always allowed
  • URLs matching the data-api-url origin are allowed
  • Blocked schemes: javascript:, data:, vbscript:, blob:
  • Cross-origin URLs not matching the above are blocked (logged to console)

Merchant ID Validation ​

Merchant IDs should be valid UUIDs. The widget passes the value directly to the API, which validates format server-side.

VGS Collect Security ​

Sensitive fields (card number, CVV, bank account/routing numbers) are rendered inside VGS Collect secure iframes. The raw values never touch your page or the PayBridge server -- they go directly to VGS for tokenization.

The VGS Collect SDK is loaded with Subresource Integrity (SRI) verification to prevent CDN tampering.

Double-Submit Prevention ​

The widget prevents double-submissions:

  • A global _widgetIsProcessing flag blocks concurrent clicks
  • The submit button is disabled with aria-busy="true" during processing
  • A beforeunload handler warns the customer if they try to navigate away mid-payment
  • Payment state is persisted to localStorage so page refresh detects in-flight payments

VGS Sandbox Warning ​

If data-vgs-environment="sandbox" is used on a non-localhost domain, a console warning is emitted. Sandbox mode will not process real payments.

Troubleshooting ​

"Configuration error: API URL must use HTTPS" ​

Cause: data-api-url uses http:// on a non-localhost domain. Fix: Set data-api-url to an https:// URL.

"Secure payment form could not load" ​

Cause: VGS Collect JS SDK failed to load. Possible reasons:

  • Network connectivity issue
  • SRI integrity check failed (CDN returned a different version)
  • data-vgs-vault-id is missing or invalid

Fix: Click the "Retry" button. If persistent, verify your VGS vault ID and network connectivity. Check the browser console for detailed errors.

Submit button stays on "Loading secure form..." ​

Cause: VGS Collect SDK loaded but failed to initialize fields. Fix: Verify data-vgs-vault-id is correct and the VGS environment matches your vault configuration.

"Your payment was declined" ​

Cause: The payment processor declined the transaction. Fix: The customer should try a different payment method or contact their bank. This is a non-retryable error.

"Too many attempts. Please wait N seconds" ​

Cause: Rate limit exceeded (HTTP 429). The widget enforces a 30-second cooldown. Fix: Wait for the countdown to expire, then click "Try Again."

"This checkout session has expired" ​

Cause: The widget token or checkout session exceeded its TTL. Fix: Generate a new widget token from your backend and re-initialize the widget.

Callbacks not firing ​

Cause: The function name in data-on-success / data-on-error is not available on window. Fix: Ensure your callback function is defined in the global scope (window.yourFunction), not inside a module or closure.

Widget not styled ​

Cause: The widget JS file was not loaded or failed to initialize. Fix: CSS is bundled into the JS file and injected at runtime. Ensure the widget script is loading:

html
<script src="https://your-host.com/widget/pb-widget.umd.js"></script>

CORS errors in browser console ​

Cause: Your domain is not in the consumer app's allowed CORS origins. Fix: Contact your PayBridge admin to add your domain to the cors_origins list for your consumer app.