Appearance
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
Script Tag (recommended)
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/widgetjs
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.
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
data-merchant | string (UUID) | Yes | -- | Merchant UUID to charge on behalf of |
data-amount | integer | Yes | -- | Amount in minor units (cents). 5000 = $50.00 |
data-currency | string | No | "USD" | ISO 4217 currency code (USD, EUR, GBP, CAD) |
data-api-url | string (URL) | Yes | "http://localhost:8000" | PayBridge API base URL. Must be HTTPS on non-localhost domains |
data-widget-token | string | Yes | -- | Short-lived HMAC-signed token from POST /widget/sessions |
data-redirect | string (URL) | No | -- | URL to redirect the customer to after successful payment |
data-vgs-vault-id | string | Yes | -- | VGS vault ID for secure iframe fields |
data-vgs-environment | string | No | "sandbox" | VGS environment: "sandbox" or "live" |
data-on-success | string | No | -- | Name of a window function to call on successful payment |
data-on-error | string | No | -- | Name of a window function to call on payment error |
data-api-key | string | No | -- | Development fallback. Use data-widget-token in production |
Amount Format
All amounts are in minor units (cents). Examples:
| Display | data-amount |
|---|---|
| $10.00 | 1000 |
| $50.00 | 5000 |
| $100.00 | 10000 |
| $999,999.99 | 99999999 |
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:
| Property | Type | Description |
|---|---|---|
transaction_id | string | PayBridge transaction UUID |
status | string | Always "success" |
amount | number | Charged 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:
| Property | Type | Description |
|---|---|---|
error_code | string | Machine-readable code (see Error Codes below) |
message | string | Human-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
| Code | Description | Retryable |
|---|---|---|
PAYMENT_DECLINED | Processor declined the payment | No |
INVALID_PAYMENT_METHOD | Card/bank details are invalid | No |
PROCESSOR_TRANSIENT | Temporary processor issue | Yes |
PROCESSOR_ERROR | Processor error | Yes |
VGS_ERROR | Secure form failed to load | Yes |
VGS_VALIDATION | Client-side field validation failed | Yes (after fixing input) |
NETWORK_ERROR | Network connectivity issue | Yes |
HTTP_429 | Rate limit exceeded (30s cooldown) | Yes (after cooldown) |
HTTP_401 | Session/token expired | No (re-create widget token) |
HTTP_408 / HTTP_504 | Request timeout | Yes |
HTTP_502 / HTTP_503 | Gateway/service unavailable | Yes |
UNKNOWN | Unexpected error | Yes |
CSS Customization
Class Names
All widget elements use the pb- prefix. Override these classes to customize appearance:
| Class | Element |
|---|---|
.pb-widget | Root container |
.pb-tabs | Tab bar container |
.pb-tab | Individual tab button |
.pb-tab.active | Active tab |
.pb-form-group | Form field wrapper |
.pb-label | Field label |
.pb-vgs-field | VGS secure iframe container |
.pb-vgs-field-error | Field with validation error |
.pb-error | Inline error message text |
.pb-row | Horizontal field row (expiry + CVC) |
.pb-col | Column within a row |
.pb-btn | Submit button |
.pb-btn:disabled | Disabled submit button |
.pb-btn-success | Button after successful payment |
.pb-spinner | Loading spinner animation |
.pb-alert | Alert message container |
.pb-alert-error | Error alert |
.pb-alert-success | Success alert |
.pb-select | Native select element (account type dropdown) |
.pb-retry-btn | Retry 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:
| Token | Default | Controls |
|---|---|---|
--pb-primary | #0078D4 | Primary button, active tab, focus ring |
--pb-primary-hover | #106EBE | Button hover |
--pb-success | #038833 | Success button, positive states |
--pb-error | #DE3131 | Error borders and text |
--pb-surface | #FFFFFF | Field backgrounds |
--pb-background | #F3F4F6 | Section background |
--pb-border | #E7E8EA | Field borders |
--pb-text | #151A24 | Labels and input text |
--pb-text-muted | #687790 | Placeholder and helper text |
--pb-text-disabled | #616161 | Disabled 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-urlorigin 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
_widgetIsProcessingflag blocks concurrent clicks - The submit button is disabled with
aria-busy="true"during processing - A
beforeunloadhandler warns the customer if they try to navigate away mid-payment - Payment state is persisted to
localStorageso 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-idis 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.