Skip to content

PayBridge Integration Guide ​

This guide walks you through integrating PayBridge into your application.

For the current client launch, the supported contract is:

  • programmatic onboarding via POST /merchants/{id}/onboard (you collect all data in your own UI) — this is the supported onboarding path
  • approval sync via merchant.approved / merchant.rejected webhooks with polling fallback
  • hosted checkout via POST /checkout/sessions

POST /merchants/{id}/onboard/hosted also exists, but it hands the application off to the processor's own form and is not the path this integration is built around.

The embeddable widgets and merchant portal remain available platform capabilities, but they are out of scope for this launch path. See the API Reference for the complete field and endpoint reference.


1. Overview ​

PayBridge is a payment-processing intermediary that handles:

  • Merchant onboarding -- programmatically creates payment processor accounts (XplorPay/Clearent) with referral attribution.
  • Checkout -- embeddable "Pay Now" widget with PCI-compliant tokenization via VGS (Very Good Security) and automatic processor failover.
  • Webhooks -- real-time notifications for payment and onboarding events forwarded to your application.

How it works:

Your App  -->  PayBridge API  -->  VGS Proxy  -->  Payment Processor
                     |                                     (Clearent)
                     |
              Webhooks back to your app

Sensitive data never touches your servers or ours. The VGS Collect JS SDK captures PII fields in secure iframes and tokenizes them before they reach the PayBridge backend. This covers both payment data (card numbers, CVVs, bank accounts) and onboarding PII (EIN, SSN, DOB, routing/account numbers). The backend passes those tokens through a VGS reverse proxy to the processor, which detokenizes them on arrival.

For full endpoint specifications, see the API Reference.


2. Prerequisites ​

Before you start, you will need:

ItemHow to get it
API KeyProvisioned by the PayBridge team. This is a X-API-Key value that identifies your consumer app.
VGS Vault IDProvided alongside your API key. Required for checkout and onboarding widgets.
Webhook endpointA publicly reachable HTTPS URL on your server to receive event notifications.
Sandbox base URLhttps://sandbox.api.nfs-pay.com
Production base URLhttps://api.nfs-pay.com

Your consumer app registration also includes:

  • CORS origins -- the domains allowed to make browser requests to the API (e.g., https://digitalshedbuilder.com). One scoped exception is handled by the API itself, not your config: it answers CORS for https://js.verygoodvault.com on the /boarding/echo path only, so VGS Collect's in-browser tokenization can read the echo response (AB#9236) — you never register the VGS origin yourself.
  • Webhook URL -- where PayBridge forwards payment and merchant status events.

Contact the PayBridge team to register your app and receive these credentials.


3. Authentication ​

All consumer-facing API requests (merchants, payments, checkout, webhooks test) require the X-API-Key header:

X-API-Key: your_api_key_here

The API key is scoped to your consumer app. All merchants and payments you create are isolated to your app.

Admin operations (creating consumer apps, managing merchants cross-app, etc.) use JWT authentication. Log in via POST /admin/auth/login with email + password to get a token, then pass it as Authorization: Bearer <jwt>. See the API Reference for details.

Rate Limits ​

Request typeLimit
Read operations (GET)100 requests / 60 seconds
Payment creation (POST /payments)10 requests / 60 seconds
Refunds (POST /payments/{id}/refund)5 requests / 60 seconds
Other write operations (POST, PUT, DELETE)20 requests / 60 seconds
Webhook endpoints200 requests / 60 seconds
Admin endpoints30 requests / 60 seconds

Rate limit headers are included in every response:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 97

When rate-limited, the API returns 429 Too Many Requests with a Retry-After header.


4. Quick Start: First Payment in 5 Steps ​

This walkthrough uses curl. Replace $API_KEY with your actual key and $BASE_URL with the appropriate environment URL.

Step 1: Create a merchant ​

bash
curl -X POST "$BASE_URL/merchants" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $API_KEY" \
  -H "X-Requested-With: PayBridge" \
  -d '{
    "name": "Digital Shed Builder",
    "business_name": "Digital Shed Builder LLC",
    "business_type": "llc",
    "ein": "27-1234567",
    "email": "billing@digitalshedbuilder.com",
    "phone": "+15551234567",
    "address": {
      "street": "100 Commerce Blvd",
      "city": "Austin",
      "state": "TX",
      "zip": "78701",
      "country": "US"
    }
  }'

Response (201):

json
{
  "id": "a1b2c3d4-...",
  "name": "Digital Shed Builder",
  "business_name": "Digital Shed Builder LLC",
  "status": "active",
  "processors": [],
  "created_at": "2026-03-10T12:00:00Z"
}

Save the id -- this is your merchant_id.

Step 2: Onboard the merchant with a processor ​

Use programmatic onboarding — the full request body and field reference are in Section 5. The hosted call below is the alternative, shown for completeness only.

bash
curl -X POST "$BASE_URL/merchants/$MERCHANT_ID/onboard/hosted" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $API_KEY" \
  -H "X-Requested-With: PayBridge" \
  -d '{
    "processor_type": "clearent",
    "is_primary": true,
    "dba_name": "Digital Shed Builder",
    "email": "billing@digitalshedbuilder.com",
    "mcc_code": "5211"
  }'

Response (202):

json
{
  "processor_id": "...",
  "type": "clearent",
  "onboarding_status": "submitted",
  "processor_merchant_id": "CLR_12345",
  "application_url": "https://boarding.clearent.net/app/...",
  "submitted_at": "2026-03-10T12:05:00Z"
}

Step 3: Wait for approval ​

In production, Clearent underwriting typically takes 1-3 business days.

The INT sandbox does fire boarding webhooks, but on its own terms. Auto-approval is triggered only when the DBA name contains [APPR]; without that tag an application never advances. With it, the first status event arrives anywhere from a few seconds to about 45 minutes after submit, and the rest — Manual Review, Approved, then Boarded with the merchant's terminal credentials — follow within a few minutes of each other. Auto-advance is also intermittent: some applications are never advanced and never rejected, and a stalled one looks exactly like a slow one.

So treat INT approval as best-effort rather than something to schedule around. The admin override endpoint (PATCH /admin/processors/{processor_id}/status with {"status":"approved"}) flips a sandbox processor to approved and fires the merchant.approved webhook to the consumer app, so partner integration testing can proceed end-to-end without waiting on INT.

Poll the status endpoint or listen for the merchant.approved webhook:

bash
curl "$BASE_URL/merchants/$MERCHANT_ID/onboard/status" \
  -H "X-API-Key: $API_KEY"

Response (200):

json
{
  "merchant_id": "a1b2c3d4-...",
  "processors": [
    {
      "processor_id": "...",
      "type": "clearent",
      "onboarding_status": "approved",
      "processor_merchant_id": "CLR_12345",
      "submitted_at": "2026-03-10T12:05:00Z",
      "approved_at": "2026-03-10T14:30:00Z"
    }
  ]
}

Step 4: Add a payment method (card) ​

bash
curl -X POST "$BASE_URL/payments/merchants/$MERCHANT_ID/payment-methods/card" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $API_KEY" \
  -H "X-Requested-With: PayBridge" \
  -d '{
    "customer_id": "cust_001",
    "card_number_token": "tok_4242xxxxxxxx4242",
    "cvv_token": "tok_cvv_xxx",
    "card_holder_token": "tok_name_xxx",
    "expires_at": "2028-06",
    "zip": "78701"
  }'

Response (201):

json
{
  "id": "pm_xyz789",
  "type": "card",
  "last_four": "4242",
  "brand": "visa",
  "expires_at": "2028-06",
  "is_active": true,
  "processor_type": "clearent",
  "created_at": "2026-03-10T12:10:00Z"
}

Note: The card_number_token, cvv_token, and card_holder_token values are VGS tokens, not raw card data. In production, these come from VGS Collect JS running in the browser. See Section 6 for widget integration.

Step 5: Process a payment ​

bash
curl -X POST "$BASE_URL/payments" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $API_KEY" \
  -H "X-Requested-With: PayBridge" \
  -d '{
    "merchant_id": "a1b2c3d4-...",
    "amount": 75000,
    "currency": "USD",
    "payment_method_id": "pm_xyz789",
    "idempotency_key": "order-1042-payment-v1",
    "description": "10x12 Shed - Order #1042",
    "invoice_number": "INV-2026-1042",
    "customer": {
      "first_name": "Jane",
      "last_name": "Smith",
      "email": "jane@example.com"
    },
    "metadata": {
      "order_id": "1042"
    }
  }'

Response (201):

json
{
  "id": "txn_abc123",
  "merchant_id": "a1b2c3d4-...",
  "amount": 75000,
  "currency": "USD",
  "status": "posted",
  "gateway_payment_id": "CLR_TXN_456",
  "payment_method_id": "pm_xyz789",
  "posted_at": "2026-03-10T12:15:00Z",
  "created_at": "2026-03-10T12:15:00Z"
}

The amount field is always in minor units (cents). 75000 = $750.00.


5. Merchant Onboarding ​

PayBridge supports two onboarding flows. Use programmatic onboarding (Option B) — it is the supported path for API integrations. Hosted onboarding hands the application off to the processor's own form and gives up the control and attribution the API exists to provide.

Option A: Hosted Onboarding (not the supported path) ​

Creates a hosted application URL at the processor. The merchant completes onboarding via the processor's own form.

Endpoint: POST /merchants/{merchant_id}/onboard/hosted

Request fields:

FieldTypeRequiredDescription
processor_typestringNo (default: "clearent")Processor to onboard with
is_primaryboolNo (default: true)Set as primary processor
dba_namestringYesDoing Business As name
emailstringYesMerchant contact email
mcc_codestringNo (default: "6513")4-digit Merchant Category Code

Response includes: application_url -- redirect the merchant to this URL to complete their application.

Submits all merchant details directly via the PayBridge API, triggering a 16-step processor onboarding sequence. Use this when you want to collect merchant data in your own UI for a consistent branded experience.

Raw onboarding PII is accepted only in sandbox/development. In sandbox and local development, sensitive fields (EIN, bank routing/account numbers, SSN, DOB) may be sent as raw values over HTTPS to simplify testing — the example below runs as-is. In staging and production, VGS tokens are required — a raw value in those environments is rejected. VGS tokenization is always required for payment card data, which is handled by the hosted checkout page.

Do not send placeholder strings shaped like tokens (tok_…) in sandbox: any value in that shape is treated as a real VGS token and routed to the tokenization proxy, which fails with Bank account creation failed: VGS proxy request failed. Either send genuine raw values or genuine tokens.

See the API Reference for a complete field reference with examples.

Endpoint: POST /merchants/{merchant_id}/onboard

Request body:

json
{
  "processor_type": "clearent",
  "is_primary": true,
  "business": {
    "legal_name": "Digital Shed Builder LLC",
    "dba_name": "Digital Shed Builder",
    "business_type": "llc",
    "ein": "27-1234567",
    "mcc_code": "5211",
    "website": "https://digitalshedbuilder.com",
    "annual_volume": 50000000,
    "average_ticket": 75000,
    "high_ticket": 250000,
    "card_present_percentage": 0
  },
  "principal": {
    "first_name": "John",
    "last_name": "Builder",
    "title": "Owner",
    "email": "john@digitalshedbuilder.com",
    "phone": "512-555-1234",
    "dob": "1985-06-15",
    "ssn": "123-45-6789",
    "ssn_last4": "6789",
    "country_of_citizenship": "US"
  },
  "banking": {
    "routing_number": "021000021",
    "account_number": "123456789",
    "account_type": "checking",
    "bank_name": "Chase"
  },
  "physical_address": {
    "line1": "100 Commerce Blvd",
    "line2": "",
    "city": "Austin",
    "state_code": "TX",
    "zip": "78701",
    "country_code": "US"
  },
  "mailing_address": null
}

Business fields:

FieldTypeRequiredDescription
legal_namestringYesLegal business name
dba_namestringYesDoing Business As name
business_typestringYesEntity type: sole_proprietorship, partnership, corporation, llc, non_profit, government, or association_estate_trust. Any other value is rejected with HTTP 422.
einstringYesTax ID / EIN (raw XX-XXXXXXX or VGS token)
mcc_codestringNo (default: "6513")4-digit Merchant Category Code
websitestringNoBusiness website URL
annual_volumeintNoEstimated annual sales volume in cents
average_ticketintNoAverage transaction amount in cents
high_ticketintNoHighest expected transaction in cents
card_present_percentageintNoCard-present percentage (0-100)

Principal fields:

FieldTypeRequiredDescription
first_namestringYesFirst name
last_namestringYesLast name
titlestringYesJob title (Owner, CEO, etc.)
emailstringYesEmail address
phonestringYesPhone number
dobstringNoDate of birth (raw YYYY-MM-DD or VGS token)
ssnstringYes for ClearentFull SSN (raw XXX-XX-XXXX with optional dashes, or VGS token) — forwarded to processors as Contact.LegalID
ssn_last4stringNoLast 4 digits of SSN — display only, not forwarded to processors
country_of_citizenshipstringNo (default: "US")Country code

Banking fields:

FieldTypeRequiredDescription
routing_numberstringYes9-digit ABA routing number (raw or VGS token)
account_numberstringYesAccount number (raw or VGS token)
account_typestringNo (default: "checking")checking or savings
bank_namestringNoName of the bank

Address fields (physical_address and optional mailing_address):

FieldTypeRequiredDescription
line1stringYesStreet address line 1
line2stringNoSuite, apt, etc.
citystringYesCity
state_codestringYes2-character state code
zipstringYesZIP / postal code (5+ chars)
country_codestringNo (default: "US")2-character country code

Resuming a failed onboarding ​

The programmatic flow runs a multi-step processor sequence in one request. If a step fails mid-flow (e.g. an invalid routing number at the bank-account step), the endpoint returns HTTP 400 and leaves the processor in draft status with its processor merchantNumber preserved. The error body identifies exactly where it stopped:

json
{
  "error": {
    "code": "BAD_REQUEST",
    "message": "[add_bank_account] Bank account creation failed: invalid routing number",
    "details": {
      "processor_id": "a1b2c3d4-...",
      "merchant_number": "7588000002352334",
      "failed_step": "add_bank_account"
    },
    "retryable": true
  }
}

To resume, fix the offending field and re-submit the same POST /merchants/{id}/onboard with the corrected payload. PayBridge detects the existing draft and resumes from where it left off — steps that already succeeded are not re-run, so no duplicate processor-side resources are created, and the original merchantNumber is reused. A successful resume returns HTTP 202 with "resumed": true:

json
{
  "processor_id": "a1b2c3d4-...",
  "type": "clearent",
  "onboarding_status": "submitted",
  "processor_merchant_id": "7588000002352334",
  "resumed": true
}

Notes:

  • Only retryable failures (status draft) resume.
  • Business identity is frozen across a resume. The legal name, DBA, business type, EIN, and owner SSNs must match the original submission — they identify the application already created at the processor. Submitting different identity values returns 409 FROZEN_IDENTITY_CHANGED. You can correct other fields (e.g. a bad routing number) on the failing step.
  • A failure at the very first step (application creation) leaves no draft to resume; simply re-submit to start a fresh application.
  • Resuming is Clearent (XplorPay) only.

Option C: Embedded Onboarding Widget ​

Drop the onboarding wizard directly into your page. It handles the full multi-step flow (Business Info, Owner Info, Banking, Review) with VGS-secured fields for all PII (EIN, SSN, DOB, routing number, account number).

html
<div id="pb-onboard"
     data-partner="your_partner_id"
     data-api-url="https://api.nfs-pay.com"
     data-api-key="your_api_key"
     data-processor-type="clearent"
     data-vgs-vault-id="your_vgs_vault_id"
     data-vgs-environment="live"
     data-redirect="https://digitalshedbuilder.com/onboarding-complete"
     data-terms-url="https://digitalshedbuilder.com/terms"
     data-privacy-url="https://digitalshedbuilder.com/privacy"
     data-support-email="support@digitalshedbuilder.com"
     data-status-callback="onOnboardingStatus">
</div>

<script src="https://api.nfs-pay.com/widget/dist/paybridge-widget.umd.js"></script>

Widget data-* attributes:

AttributeRequiredDescription
data-partnerNoYour referral partner ID for revenue attribution
data-api-urlYesPayBridge API base URL
data-api-keyYesYour consumer app API key
data-processor-typeNo (default: "clearent")Processor type
data-vgs-vault-idYesVGS vault ID for secure bank fields
data-vgs-environmentNo (default: "sandbox")sandbox or live
data-redirectNoURL to redirect to after approval
data-terms-urlNoTerms of Service URL (shown on review step)
data-privacy-urlNoPrivacy Policy URL (shown on review step)
data-support-emailNoSupport email displayed on confirmation screen
data-status-callbackNoJavaScript function name on window called with { status, merchantId }

Status callback example:

javascript
function onOnboardingStatus(event) {
  console.log("Status:", event.status);       // "submitted", "approved", "rejected", "timeout"
  console.log("Merchant:", event.merchantId); // PayBridge merchant ID
}

Option D: Custom Onboarding Form with VGS Collect ​

Build your own onboarding UI while keeping all PII secure. Use VGS Collect JS to render secure iframes for sensitive fields. Your servers and PayBridge only ever see tokens -- VGS detokenizes when forwarding to the processor.

Which fields MUST use VGS Collect iframes:

FieldVGS field nameValidation
EINein/^\d{2}-?\d{7}$/
SSN (full)ssn/^\d{3}-?\d{2}-?\d{4}$/
Date of birthdob/^\d{4}-\d{2}-\d{2}$/
Routing numberrouting_number/^\d{9}$/
Account numberaccount_number/^\d{4,17}$/

All other fields (business name, address, phone, email, etc.) are normal form inputs on your page.

Step 1: Load VGS Collect JS

html
<script src="https://js.verygoodvault.com/vgs-collect/2.24.0/vgs-collect.js"></script>

CSP requirements (add to your Content-Security-Policy header):

script-src https://js.verygoodvault.com;
frame-src https://js.verygoodvault.com;

Step 2: Create VGS Collect instances

Each logical group of sensitive fields needs its own VGS Collect instance:

javascript
const vaultId = "your_vgs_vault_id";  // provided by PayBridge team
const env = "sandbox";                 // "sandbox" or "live"

// Business PII (EIN)
// The third argument (a state callback) is required: omitting it throws
// "callback is not a function" at init. Pass a no-op if you don't need state.
const businessCollect = VGSCollect.create(vaultId, env, () => {});
businessCollect.field("#ein-container", {
  type: "text",
  name: "ein",
  placeholder: "27-1234567",
  validations: ["required", "/^\\d{2}-?\\d{7}$/"],
  css: { fontSize: "14px", padding: "8px" },
});

// Principal PII (DOB, SSN)
const principalCollect = VGSCollect.create(vaultId, env, () => {});
principalCollect.field("#dob-container", {
  type: "text",
  name: "dob",
  placeholder: "YYYY-MM-DD",
  validations: ["/^\\d{4}-\\d{2}-\\d{2}$/"],
  css: { fontSize: "14px", padding: "8px" },
});
principalCollect.field("#ssn-container", {
  type: "text",
  name: "ssn",
  placeholder: "123-45-6789",
  validations: ["required", "/^\\d{3}-?\\d{2}-?\\d{4}$/"],
  css: { fontSize: "14px", padding: "8px" },
});

// Banking PII (routing number, account number)
const bankingCollect = VGSCollect.create(vaultId, env, () => {});
bankingCollect.field("#routing-container", {
  type: "text",
  name: "routing_number",
  placeholder: "123456789",
  validations: ["required", "/^\\d{9}$/"],
  css: { fontSize: "14px", padding: "8px" },
});
bankingCollect.field("#account-container", {
  type: "text",
  name: "account_number",
  placeholder: "000123456789",
  validations: ["required", "/^\\d{4,17}$/"],
  css: { fontSize: "14px", padding: "8px" },
});

Step 3: Submit and collect tokens

Before calling the PayBridge API, submit each VGS Collect instance to tokenize:

javascript
function vgsSubmit(collectInstance) {
  return new Promise((resolve, reject) => {
    collectInstance.submit("/post", {}, (status, data) => {
      if (status >= 200 && status < 300) resolve(data);
      else reject(new Error("Tokenization failed"));
    });
  });
}

async function submitOnboarding() {
  // Tokenize all sensitive fields (returns VGS tokens)
  const [businessTokens, principalTokens, bankingTokens] = await Promise.all([
    vgsSubmit(businessCollect),
    vgsSubmit(principalCollect),
    vgsSubmit(bankingCollect),
  ]);

  // Merge tokens with your plain form data
  const payload = {
    processor_type: "clearent",
    is_primary: true,
    business: {
      legal_name: document.getElementById("legal_name").value,
      dba_name: document.getElementById("dba_name").value,
      business_type: document.getElementById("business_type").value,
      ...businessTokens,   // { ein: "tok_sandbox_ein_abc123" }
    },
    principal: {
      first_name: document.getElementById("first_name").value,
      last_name: document.getElementById("last_name").value,
      title: document.getElementById("title").value,
      email: document.getElementById("email").value,
      phone: document.getElementById("phone").value,
      ...principalTokens,  // { dob: "tok_...", ssn: "tok_..." }
    },
    banking: {
      account_type: document.getElementById("account_type").value,
      bank_name: document.getElementById("bank_name").value,
      ...bankingTokens,    // { routing_number: "tok_...", account_number: "tok_..." }
    },
    physical_address: { /* ... normal form fields ... */ },
  };

  // POST to PayBridge API
  const res = await fetch(`${apiUrl}/merchants/${merchantId}/onboard`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-API-Key": apiKey, "X-Requested-With": "PayBridge" },
    body: JSON.stringify(payload),
  });
}

Step 4: HTML containers

VGS Collect renders iframes inside empty <div> elements. Style the containers, not the iframes:

html
<!-- Sensitive fields — VGS Collect iframes -->
<div id="ein-container" style="height:40px; border:1px solid #ccc; border-radius:4px;"></div>
<div id="dob-container" style="height:40px; border:1px solid #ccc; border-radius:4px;"></div>
<div id="ssn-container" style="height:40px; border:1px solid #ccc; border-radius:4px;"></div>
<div id="routing-container" style="height:40px; border:1px solid #ccc; border-radius:4px;"></div>
<div id="account-container" style="height:40px; border:1px solid #ccc; border-radius:4px;"></div>

<!-- Normal fields — regular <input> elements -->
<input type="text" id="legal_name" placeholder="Legal Business Name">
<input type="text" id="dba_name" placeholder="DBA Name">
<!-- ... etc ... -->

Important: VGS iframe values cannot be read back by JavaScript on the host page. If you have a review/confirmation step, show masked placeholders (e.g., "EIN: Provided securely", "SSN: ****") instead of the raw values.

Checking Onboarding Status ​

Endpoint: GET /merchants/{merchant_id}/onboard/status

This endpoint returns the merchant's onboarding status as persisted from the processor's boarding webhook — the webhook is the authoritative source of status transitions, so this read reflects the latest webhook-confirmed status.

Possible onboarding_status values:

StatusMeaning
draftA boarding step failed partway. Recoverable — re-submit POST /merchants/{id}/onboard to resume from where it stopped (see Resuming a failed onboarding).
submittedApplication submitted, awaiting review
pending_reviewUnder review by the processor
approvedMerchant account is active and can process payments
rejectedApplication was denied

Document Upload (KYC) Widget ​

During underwriting the processor may request supporting documents (bank statements, ID, proof of address). Embed the document-upload widget so the merchant's files go straight from their browser to PayBridge — your servers never touch them. The widget lists the documents that are still outstanding, shows the instructions attached to each request, and uploads each one.

Unlike the onboarding widget, this surface authenticates with a short-lived widget token (not your API key), scoped to a single merchant and the upload_documents action. Mint it server-to-server, then pass it to the browser.

1. Mint a widget token (server-to-server, never expose your API key in the browser):

bash
curl -X POST "$BASE_URL/widget/sessions" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $API_KEY" \
  -H "X-Requested-With: PayBridge" \
  -d '{
    "merchant_id": "a1b2c3d4-5678-9abc-def0-1234567890ab",
    "allowed_actions": ["upload_documents"],
    "ttl_seconds": 900
  }'
# → { "widget_token": "wt1...", "expires_at": "2026-03-10T12:15:00Z" }

2. Embed the widget with that token:

html
<div id="pb-kyc"
     data-api-url="https://api.nfs-pay.com"
     data-widget-token="wt1..."
     data-on-uploaded="onDocUploaded"
     data-on-error="onDocError">
</div>

<script src="https://api.nfs-pay.com/widget/dist/paybridge-widget.umd.js"></script>

Widget data-* attributes:

AttributeRequiredDescription
data-api-urlYesPayBridge API base URL
data-widget-tokenYesShort-lived widget token granted the upload_documents action (from POST /widget/sessions)
data-on-uploadedNowindow-level JS callback name, invoked with { document_id } after a successful upload
data-on-errorNowindow-level JS callback name, invoked with { document_id, message } on failure

The widget reads the outstanding documents from GET /widget/documents and uploads to POST /widget/documents/{document_id}/upload with the widget token — your API key is never exposed in the browser. Document-lifecycle changes (document.uploaded, document.accepted, document.rejected) are also delivered to your registered webhook URL; see the Webhook guide.


6. Payment Processing ​

The checkout widget handles VGS tokenization, payment submission, error display, and redirect -- all in a single HTML snippet. It supports both card and bank account payments via tabbed UI.

html
<div id="pb-checkout"
     data-merchant="your_merchant_id"
     data-amount="75000"
     data-currency="USD"
     data-api-url="https://api.nfs-pay.com"
     data-api-key="your_api_key"
     data-vgs-vault-id="your_vgs_vault_id"
     data-vgs-environment="live"
     data-redirect="https://digitalshedbuilder.com/thank-you"
     data-on-success="onPaymentSuccess"
     data-on-error="onPaymentError">
</div>

<script src="https://api.nfs-pay.com/widget/dist/paybridge-widget.umd.js"></script>

Widget data-* attributes:

AttributeRequiredDescription
data-merchantYesMerchant ID (from Step 1)
data-amountYesAmount in cents (e.g., 75000 = $750.00)
data-currencyNo (default: "USD")ISO 4217 currency code
data-api-urlYesPayBridge API base URL
data-api-keyYesYour consumer app API key
data-vgs-vault-idYesVGS vault ID for secure card/bank fields
data-vgs-environmentNo (default: "sandbox")sandbox or live
data-redirectNoURL to redirect to after successful payment
data-on-successNoJavaScript function name on window for success callback
data-on-errorNoJavaScript function name on window for error callback

Callback examples:

javascript
function onPaymentSuccess(data) {
  // data = { transaction_id: "txn_...", status: "success", amount: 75000 }
  console.log("Payment succeeded:", data.transaction_id);
}

function onPaymentError(data) {
  // data = { error_code: "PAYMENT_DECLINED", message: "Your payment was declined..." }
  console.log("Payment failed:", data.error_code, data.message);
}

Redirect behavior: On success, the widget redirects to data-redirect with query parameters:

  • pay_txn_id -- transaction ID
  • pay_status -- "success"
  • pay_amount -- amount in cents

Example: https://digitalshedbuilder.com/thank-you?pay_txn_id=txn_abc123&pay_status=success&pay_amount=75000

Security note: The widget validates redirect URLs against the embedding page's origin. Cross-origin redirects, javascript: URIs, and data: URIs are blocked. The data-redirect URL must be same-origin as the page embedding the widget or match the data-api-url origin.

Option B: Direct API (Advanced) ​

If you need full control over the UI, call the API directly. You are responsible for VGS Collect integration on your frontend.

Add a card payment method ​

POST /payments/merchants/{merchant_id}/payment-methods/card

json
{
  "customer_id": "cust_001",
  "card_number_token": "tok_4242xxxxxxxx4242",
  "cvv_token": "tok_cvv_xxx",
  "card_holder_token": "tok_name_xxx",
  "expires_at": "2028-06",
  "zip": "78701",
  "billing_address": {
    "street": "456 Oak Ave",
    "city": "Austin",
    "state": "TX",
    "zip": "78702",
    "country": "US"
  }
}
FieldTypeRequiredDescription
customer_idstringYesYour customer identifier
card_number_tokenstringYesVGS token for card number
cvv_tokenstringYesVGS token for CVV
card_holder_tokenstringYesVGS token for cardholder name
expires_atstringYesExpiry in YYYY-MM format
zipstringYesBilling ZIP code
billing_addressobjectNoFull billing address

Add a bank payment method ​

POST /payments/merchants/{merchant_id}/payment-methods/bank

json
{
  "customer_id": "cust_001",
  "account_holder_token": "tok_holder_xxx",
  "account_number_token": "tok_acct_xxx",
  "routing_number_token": "tok_rout_xxx",
  "account_type": "checking"
}
FieldTypeRequiredDescription
customer_idstringYesYour customer identifier
account_holder_tokenstringYesVGS token for account holder name
account_number_tokenstringYesVGS token for account number
routing_number_tokenstringYesVGS token for routing number
account_typestringNo (default: "checking")checking or savings

Process a payment ​

POST /payments

json
{
  "merchant_id": "a1b2c3d4-...",
  "amount": 75000,
  "currency": "USD",
  "payment_method_id": "pm_xyz789",
  "description": "10x12 Shed - Order #1042",
  "invoice_number": "INV-2026-1042",
  "customer": {
    "first_name": "Jane",
    "last_name": "Smith",
    "email": "jane@example.com"
  },
  "billing_address": {
    "street": "456 Oak Ave",
    "city": "Austin",
    "state": "TX",
    "zip": "78702",
    "country": "US"
  },
  "metadata": {
    "order_id": "1042"
  }
}
FieldTypeRequiredDescription
merchant_idstringYesMerchant ID
amountintYesAmount in cents
currencystringNo (default: "USD")ISO 4217 currency code
payment_method_idstringYes*Payment method ID (from add card/bank)
idempotency_keystringYesUnique key for this request (prevents double-charges on retry). Use your order ID or a UUID. Max 255 chars.
descriptionstringNoPayment description
invoice_numberstringNoInvoice reference
customerobjectNoCustomer info (first_name, last_name, email)
billing_addressobjectNoBilling address
metadataobjectNoArbitrary key-value pairs for your records
customer_emailstringNoCustomer email (for portable methods)
customer_payment_method_idstringNo*Portable payment method ID (alternative to payment_method_id)

* Exactly one of payment_method_id or customer_payment_method_id is required.

Stored bank (ACH) methods. Charging a stored bank method here is not yet supported and returns a 400 — a bank charge needs the raw VGS routing/account tokens, which a stored method does not hold. The supported ACH path is the hosted checkout, where those tokens are collected at pay time. Card methods are unaffected (AB#9519).

Payment statuses:

StatusMeaning
postedPayment completed successfully
declinedDeclined by processor
failedTransient processor error
refundedPayment was refunded

Multi-gateway failover: If the primary processor returns a transient error and a secondary processor is configured, PayBridge automatically retries on the secondary. Terminal errors (insufficient funds, invalid card) are not retried. If failover occurred, the response includes failover_from_id.

Issue a refund ​

POST /payments/{payment_id}/refund

json
{
  "amount": 25000,
  "idempotency_key": "refund-order-1042-v1",
  "description": "Partial refund - customer credit"
}
FieldTypeRequiredDescription
amountintNoRefund amount in cents. Omit for full refund.
idempotency_keystringYesUnique key for this refund request (prevents duplicate refunds on retry).
descriptionstringNoReason for refund

Retrieve a payment ​

GET /payments/{payment_id}

Returns the full transaction record including status, gateway_payment_id, posted_at, and failover_from_id.

The simplest integration path. Your backend creates a checkout session, then redirects the customer to the hosted checkout URL. PayBridge handles the payment form, VGS tokenization, and payment processing. No frontend integration required on your side.

Step 1: Create a checkout session ​

bash
curl -X POST "$BASE_URL/checkout/sessions" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $API_KEY" \
  -H "X-Requested-With: PayBridge" \
  -d '{
    "merchant_id": "a1b2c3d4-...",
    "amount": 75000,
    "currency": "USD",
    "success_url": "https://yoursite.com/payment-success",
    "cancel_url": "https://yoursite.com/payment-cancelled",
    "description": "Order #12345",
    "customer_email": "customer@example.com"
  }'

Response (201):

json
{
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "checkout_url": "https://api.nfs-pay.com/checkout/session/f47ac10b-58cc-4372-a567-0e02b2c3d479?st=v1.1710168600.abc123def456...",
  "amount": 75000,
  "currency": "USD",
  "status": "pending",
  "expires_at": "2026-03-11T15:30:00Z",
  "created_at": "2026-03-11T15:00:00Z"
}

Fields:

FieldTypeRequiredDescription
merchant_idstringYesMerchant ID
amountintYesAmount in cents (min 1, max 99999999)
currencystringNoDefault "USD"
success_urlstringYesWhere to redirect after successful payment
cancel_urlstringNoWhere to redirect on cancellation
descriptionstringNoPayment description (max 500 chars)
customer_emailstringNoPre-fill customer email on checkout page
idempotency_keystringNoPrevents duplicate sessions on retry

Security: success_url and cancel_url must match your app's registered cors_origins. This prevents open-redirect attacks.

Step 2: Redirect the customer ​

Redirect your customer to the checkout_url from the response. The hosted checkout page handles:

  • Card/bank entry via VGS Collect (PCI-compliant)
  • Payment submission
  • Error display and retry
  • Redirect to your success_url on success or cancel_url on cancel

Step 3: Handle the redirect ​

After payment, the customer is redirected to your success_url with query parameters:

https://yoursite.com/payment-success?pay_session_id=f47ac10b-58cc-4372-a567-0e02b2c3d479&pay_status=success

Step 4: Verify payment status ​

Poll the session status to confirm payment:

bash
curl "$BASE_URL/checkout/sessions/$SESSION_ID" \
  -H "X-API-Key: $API_KEY"

Response:

json
{
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "status": "completed",
  "amount": 75000,
  "currency": "USD",
  "merchant_id": "a1b2c3d4-...",
  "payment_id": "txn_abc123",
  "expires_at": "2026-03-11T15:30:00Z",
  "completed_at": "2026-03-11T15:05:00Z"
}

Session statuses: pending → completed | expired

You can also manually expire a pending session:

bash
curl -X POST "$BASE_URL/checkout/sessions/$SESSION_ID/expire" \
  -H "X-API-Key: $API_KEY" \
  -H "X-Requested-With: PayBridge"

Note: Sessions expire automatically after 30 minutes.

Transaction History ​

List transactions for a merchant with optional filtering and pagination:

bash
curl "$BASE_URL/payments/merchants/$MERCHANT_ID/transactions?status=posted&limit=20&offset=0" \
  -H "X-API-Key: $API_KEY"

Response:

json
{
  "items": [
    {
      "id": "txn_abc123",
      "merchant_id": "a1b2c3d4-...",
      "amount": 75000,
      "currency": "USD",
      "status": "posted",
      "description": "Order #12345",
      "posted_at": "2026-03-11T15:05:00Z",
      "created_at": "2026-03-11T15:00:00Z"
    }
  ],
  "total": 42,
  "limit": 20,
  "offset": 0
}

Query parameters:

ParamTypeDefaultDescription
statusstring—Filter by status (pending, posted, declined, refunded, etc.)
limitint50Page size (1–100)
offsetint0Number of items to skip

Payment Methods ​

List active payment methods for a merchant:

bash
curl "$BASE_URL/payments/merchants/$MERCHANT_ID/payment-methods" \
  -H "X-API-Key: $API_KEY"

Response:

json
[
  {
    "id": "pm_abc123",
    "type": "card",
    "last_four": "4242",
    "brand": "visa",
    "expires_at": "2028-12",
    "is_active": true,
    "created_at": "2026-03-10T12:00:00Z"
  }
]

7. Webhooks ​

PayBridge forwards processor events to your registered webhook URL as standardized JSON payloads.

Event Types ​

Payment events (forwarded to your webhook URL):

EventTrigger
payment.completedPayment settled or approved by processor
payment.declinedPayment declined by processor
payment.refundedRefund processed

Merchant events (forwarded to your webhook URL):

EventTrigger
merchant.approvedMerchant onboarding application approved
merchant.rejectedMerchant onboarding application rejected

Checkout events (forwarded to your webhook URL):

EventTrigger
checkout.completedHosted checkout session payment succeeded

Payload Format ​

Payment event:

json
{
  "event_type": "payment.completed",
  "webhook_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "payment_id": "txn_abc123",
  "merchant_id": "a1b2c3d4-...",
  "amount": 75000,
  "status": "posted",
  "timestamp": "2026-03-10T12:15:30.123456+00:00"
}

Merchant event:

json
{
  "event_type": "merchant.approved",
  "webhook_id": "a23bc45d-67ef-8901-a234-5b6c7d8e9f01",
  "merchant_id": "a1b2c3d4-...",
  "processor_type": "clearent",
  "processor_merchant_id": "CLR_12345",
  "onboarding_status": "approved",
  "timestamp": "2026-03-11T14:00:00.000000+00:00"
}

Checkout event:

json
{
  "event_type": "checkout.completed",
  "webhook_id": "c45de67f-8901-2bcd-ef34-567890123456",
  "session_id": "session-uuid",
  "payment_id": "payment-uuid",
  "merchant_id": "a1b2c3d4-...",
  "amount": 75000,
  "currency": "USD",
  "status": "posted",
  "timestamp": "2026-03-26T12:02:30.000000+00:00"
}

Note: The webhook_id field is a unique identifier for the webhook delivery. Use it as a deduplication key to prevent processing the same event twice on retries.

Responding to Webhooks ​

Your endpoint should return a 2xx status code to acknowledge receipt. PayBridge currently delivers webhooks on a best-effort basis (fire-and-forget with a 10-second timeout). For guaranteed delivery, poll the status APIs as a fallback.

Setting Your Webhook URL ​

Your webhook URL is configured when your consumer app is provisioned. To update it, contact the PayBridge team or use the admin API (requires admin JWT from POST /admin/auth/login):

bash
curl -X PUT "$BASE_URL/admin/apps/$APP_ID/webhook-url" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_JWT" \
  -d '{ "webhook_url": "https://digitalshedbuilder.com/api/pb-webhooks" }'

Signature Verification ​

Every outbound webhook includes an X-PayBridge-Signature header containing an HMAC-SHA256 signature of the request body. Always verify this signature to ensure the webhook was sent by PayBridge and was not tampered with.

The signature format is: sha256=<hex-encoded HMAC-SHA256>

The HMAC is computed over the raw JSON request body using your app's webhook_secret (returned when your consumer app is provisioned via POST /admin/apps).

Verification steps:

  1. Read the raw request body bytes (do not parse JSON first).
  2. Compute HMAC-SHA256 of the body using your webhook_secret.
  3. Compare your computed signature with the X-PayBridge-Signature header using a constant-time comparison.
  4. Reject the request if the signatures do not match.

Additional recommendations:

  • Use HTTPS for your webhook endpoint to prevent tampering in transit.
  • Validate that the merchant_id in the payload belongs to your application.

Idempotency: Use the webhook_id field in the payload as a deduplication key. PayBridge deduplicates inbound processor webhooks, but as a best practice, implement idempotent handling on your side to gracefully handle retries.

Example: Python webhook handler ​

python
import hashlib
import hmac
import json

from fastapi import FastAPI, Header, HTTPException, Request

app = FastAPI()

WEBHOOK_SECRET = "your-webhook-secret-from-provisioning"


def verify_signature(body: bytes, signature: str, secret: str) -> bool:
    """Verify X-PayBridge-Signature header."""
    expected = "sha256=" + hmac.new(
        secret.encode(), body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)


@app.post("/api/pb-webhooks")
async def handle_pb_webhook(
    request: Request,
    x_paybridge_signature: str = Header(..., alias="X-PayBridge-Signature"),
):
    body = await request.body()

    if not verify_signature(body, x_paybridge_signature, WEBHOOK_SECRET):
        raise HTTPException(status_code=401, detail="Invalid signature")

    payload = json.loads(body)
    event_type = payload.get("event_type")
    webhook_id = payload.get("webhook_id")

    # Deduplicate using webhook_id
    # if already_processed(webhook_id): return {"status": "ok"}

    if event_type == "payment.completed":
        payment_id = payload["payment_id"]
        amount = payload["amount"]
        # Update your order status, send receipt, etc.
        print(f"Payment {payment_id} completed: ${amount / 100:.2f}")

    elif event_type == "payment.declined":
        payment_id = payload["payment_id"]
        # Notify customer, update order status
        print(f"Payment {payment_id} declined")

    elif event_type == "payment.refunded":
        payment_id = payload["payment_id"]
        amount = payload["amount"]
        print(f"Payment {payment_id} refunded: ${amount / 100:.2f}")

    elif event_type == "merchant.approved":
        merchant_id = payload["merchant_id"]
        # Enable payment processing for this merchant
        print(f"Merchant {merchant_id} approved")

    elif event_type == "merchant.rejected":
        merchant_id = payload["merchant_id"]
        # Notify merchant, suggest re-application
        print(f"Merchant {merchant_id} rejected")

    return {"status": "ok"}

Example: Node.js webhook handler ​

javascript
const express = require("express");
const crypto = require("crypto");

const app = express();
// Use raw body for signature verification
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));

const WEBHOOK_SECRET = "your-webhook-secret-from-provisioning";

function verifySignature(rawBody, signature, secret) {
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

app.post("/api/pb-webhooks", (req, res) => {
  const signature = req.headers["x-paybridge-signature"];
  if (!signature || !verifySignature(req.rawBody, signature, WEBHOOK_SECRET)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  const { event_type, webhook_id, payment_id, merchant_id, amount } = req.body;

  // Deduplicate using webhook_id
  // if (alreadyProcessed(webhook_id)) return res.json({ status: "ok" });

  switch (event_type) {
    case "payment.completed":
      console.log(`Payment ${payment_id} completed: $${(amount / 100).toFixed(2)}`);
      // Update order status, send receipt
      break;

    case "payment.declined":
      console.log(`Payment ${payment_id} declined`);
      // Notify customer
      break;

    case "payment.refunded":
      console.log(`Payment ${payment_id} refunded: $${(amount / 100).toFixed(2)}`);
      break;

    case "merchant.approved":
      console.log(`Merchant ${merchant_id} approved`);
      // Enable payment processing
      break;

    case "merchant.rejected":
      console.log(`Merchant ${merchant_id} rejected`);
      break;

    default:
      console.log(`Unknown event: ${event_type}`);
  }

  res.json({ status: "ok" });
});

app.listen(3000);

8. Testing ​

Sandbox Environment ​

ResourceURL
APIhttps://sandbox.api.nfs-pay.com
API Docs (Swagger)https://sandbox.api.nfs-pay.com/docs
Widget JShttps://sandbox.api.nfs-pay.com/widget/dist/paybridge-widget.umd.js
VGS Environmentsandbox

Use your sandbox API key for all test requests. Sandbox transactions do not hit real processors.

Test Scenarios ​

Checkout widget:

  1. Embed the checkout widget with data-vgs-environment="sandbox" and your sandbox VGS vault ID.
  2. Fill in test card details in the VGS Collect iframes.
  3. Verify the success callback fires and the redirect includes pay_txn_id.
  4. Test error handling: use an invalid amount (0 or negative) to trigger a validation error.

Onboarding widget:

  1. Embed the onboarding widget with sandbox credentials.
  2. Walk through all four steps (Business Info, Owner Info, Banking, Review).
  3. Verify the status polling screen appears after submission.
  4. Confirm the data-status-callback function is called with { status: "submitted", merchantId: "..." }.

API direct:

  1. Create a merchant via POST /merchants.
  2. Submit onboarding via POST /merchants/{id}/onboard/hosted.
  3. Poll GET /merchants/{id}/onboard/status until status changes.
  4. Add a payment method and process a test payment.
  5. Issue a refund.

Error Codes to Test Against ​

ScenarioExpected Error CodeHTTP Status
Missing API keyUNAUTHORIZED401
Invalid API keyUNAUTHORIZED401
Merchant not foundRESOURCE_NOT_FOUND404
Invalid request bodyVALIDATION_ERROR422
Payment declinedPAYMENT_DECLINED402
Processor errorPROCESSOR_ERROR502
Rate limit exceededRATE_LIMITED429

9. Production VGS Setup ​

In production, sensitive fields are tokenized by VGS Collect JS running in the customer's browser (live environment) before they ever reach PayBridge. PayBridge operates the VGS vault and both proxy directions — you only point VGS Collect at the production vault ID and environment we issue you, then send the resulting tokens to the API.

Responsibility split ​

PayBridge managesYou configure
The VGS vault and accountLoad VGS Collect JS on your page
Inbound tokenization routes (field-name → token filters)Initialize VGS Collect with the production vault ID + live environment
The outbound forward proxy that detokenizes when calling the processorSubmit the secure fields to get tokens, then POST those tokens to the PayBridge API
CA-certificate pinning and proxy credentials—

You never call the VGS forward proxy or configure detokenization — that is entirely server-side in PayBridge.

Production vault configuration ​

Your production VGS values are issued alongside your production API key. The values below are placeholders — substitute the real ones we provide (no real vault IDs or credentials live in this guide):

SettingProduction valueNotes
Vault IDtnt_xxxxxxxxxxIssued with your production API key
EnvironmentliveSandbox uses sandbox
VGS Collect JShttps://js.verygoodvault.com/vgs-collect/2.24.0/vgs-collect.jsSame SDK in all environments
Inbound route (tokenize)https://tnt_xxxxxxxxxx.live.verygoodproxy.comVGS Collect posts here automatically on .submit(); sandbox uses .sandbox.verygoodproxy.com

Initialize VGS Collect with the production vault ID and live environment:

javascript
const vaultId = "tnt_xxxxxxxxxx"; // production vault ID issued by PayBridge
const env = "live";               // "sandbox" for testing
// The third argument (a state callback) is required: omitting it throws
// "callback is not a function" at init. Pass a no-op if you don't need state.
const collect = VGSCollect.create(vaultId, env, () => {});

If you embed the widgets instead of building your own form, set data-vgs-vault-id="tnt_xxxxxxxxxx" and data-vgs-environment="live".

CSP requirements (add to your Content-Security-Policy header):

script-src https://js.verygoodvault.com;
frame-src https://js.verygoodvault.com;

What must be tokenized ​

DataTokenization
Card number, CVV, cardholder nameRequired — handled by the checkout widget / hosted checkout (see Section 6)
Onboarding PII (EIN, SSN, DOB, routing number, account number)Required in staging and production — sandbox and development also accept raw values over HTTPS (see Option B)

Tokenizing onboarding PII keeps raw EIN/SSN/bank numbers off both your servers and ours (SAQ-A scope). The names below are the VGS Collect field names and the JSON keys you send to POST /merchants/{merchant_id}/onboard:

FieldVGS Collect / JSON nameRaw format
EINeinXX-XXXXXXX
SSN (full)ssnXXX-XX-XXXX (dashes optional)
Date of birthdobYYYY-MM-DD
Routing numberrouting_number9 digits
Account numberaccount_number4–17 digits

VGS tokens are returned in the form tok_…. ssn_last4 is display-only and is never tokenized or forwarded to the processor.

The full frontend walkthrough — creating VGS Collect instances, mounting iframe containers, and submitting to retrieve tokens — is in Option D: Custom Onboarding Form with VGS Collect. This section covers only the production configuration and the resulting payload.

Sample tokenized request ​

A production onboarding request with VGS tokens in place of raw PII (POST /merchants/{merchant_id}/onboard). Token values are illustrative placeholders:

json
{
  "processor_type": "clearent",
  "is_primary": true,
  "business": {
    "legal_name": "Digital Shed Builder LLC",
    "dba_name": "Digital Shed Builder",
    "business_type": "llc",
    "ein": "tok_live_ein_a1b2c3d4e5",
    "mcc_code": "5211"
  },
  "principal": {
    "first_name": "John",
    "last_name": "Builder",
    "title": "Owner",
    "email": "john@digitalshedbuilder.com",
    "phone": "+15551234567",
    "dob": "tok_live_dob_f6g7h8i9j0",
    "ssn": "tok_live_ssn_k1l2m3n4o5",
    "ssn_last4": "6789",
    "country_of_citizenship": "US"
  },
  "banking": {
    "routing_number": "tok_live_rout_p6q7r8s9t0",
    "account_number": "tok_live_acct_u1v2w3x4y5",
    "account_type": "checking",
    "bank_name": "Chase"
  },
  "physical_address": {
    "line1": "100 Commerce Blvd",
    "city": "Austin",
    "state_code": "TX",
    "zip": "78701",
    "country_code": "US"
  },
  "mailing_address": null
}

Headers: X-API-Key: <your-production-key>, Content-Type: application/json, and X-Requested-With: PayBridge.

For the checkout (payment) path, the equivalent token fields are card_number_token / cvv_token / card_holder_token (card) and account_holder_token / account_number_token / routing_number_token (bank) — see Section 6.


10. Going Live Checklist ​

Before switching from sandbox to production:

  • [ ] API key: Obtain your production API key from the PayBridge team.
  • [ ] VGS vault ID: Switch to the production VGS vault ID and set the VGS environment to live — see Production VGS Setup.
  • [ ] Base URL: Update all API calls and widget data-api-url from sandbox.api.nfs-pay.com to api.nfs-pay.com.
  • [ ] CORS origins: Confirm your production domain(s) are registered.
  • [ ] Webhook URL: Register your production webhook endpoint (must be HTTPS).
  • [ ] Webhook handling: Verify your endpoint returns 2xx for all event types and handles unknown events gracefully.
  • [ ] Idempotency: Confirm your webhook handler is idempotent (safe to receive the same event twice).
  • [ ] Error handling: Verify your UI handles all error states (PAYMENT_DECLINED, PROCESSOR_ERROR, RATE_LIMITED, network failures).
  • [ ] Amounts: Double-check that all amounts are in cents (minor units). $750.00 = 75000.
  • [ ] Refund flow: Test full and partial refunds in sandbox before going live.
  • [ ] Merchant onboarding: Ensure at least one merchant is fully onboarded and approved on the production processor.
  • [ ] Security: Confirm API keys are stored server-side (environment variables or secrets manager), never exposed in client-side code other than widget data-api-key attributes.

11. Support ​

ChannelContact
Technical supportsupport@nfs-pay.com
Integration questionsReach out to your PayBridge integration contact
API statusGET /health (liveness) and GET /ready (full readiness)
API documentationAPI Reference (every environment); interactive Swagger at https://sandbox.api.nfs-pay.com/docs in sandbox

Error Response Format ​

All API errors follow a consistent structure:

json
{
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable description",
    "details": {}
  }
}

See the API Reference for the full list of error codes.