Appearance
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.rejectedwebhooks with polling fallback - hosted checkout via
POST /checkout/sessions
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 appSensitive 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:
| Item | How to get it |
|---|---|
| API Key | Provisioned by the PayBridge team. This is a X-API-Key value that identifies your consumer app. |
| VGS Vault ID | Provided alongside your API key. Required for checkout and onboarding widgets. |
| Webhook endpoint | A publicly reachable HTTPS URL on your server to receive event notifications. |
| Sandbox base URL | https://sandbox.api.nfs-pay.com |
| Production base URL | https://api.shedpay.us |
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 forhttps://js.verygoodvault.comon the/boarding/echopath 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_hereThe 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 type | Limit |
|---|---|
| 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 endpoints | 200 requests / 60 seconds |
| Admin endpoints | 30 requests / 60 seconds |
Rate limit headers are included in every response:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 97When 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:00"
}Save the id -- this is your merchant_id.
Step 2: Onboard the merchant with a processor
Submit the full application with POST /merchants/{merchant_id}/onboard — the request body and field reference are in Section 5. The response is 202 with the processor record in submitted status.
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. If a sandbox merchant stalls, ask your integration contact to approve it: the PayBridge team can flip a sandbox processor to approved from the admin side (your API key cannot), which fires the merchant.approved webhook to your app, so 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:00",
"approved_at": "2026-03-10T14:30:00"
}
]
}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:00"
}customer_id is your own ID for the customer, whatever your system already calls them. PayBridge keeps the customer's record at the processor for you, so you never need a processor customer key. Saving another card or bank account with the same customer_id adds it to the same customer.
The id in the response is what you charge: pass it as payment_method_id to POST /payments for this merchant (Step 5). A method saved this way can be charged only by your own merchants.
Note: The
card_number_token,cvv_token, andcard_holder_tokenvalues 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:00",
"created_at": "2026-03-10T12:15:00"
}The amount field is always in minor units (cents). 75000 = $750.00.
5. Merchant Onboarding
Submits all merchant details directly via the PayBridge API, triggering a 16-step processor onboarding sequence. This is the only supported onboarding path: you collect merchant data in your own UI, so the experience stays branded and the merchant stays attributed to you.
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": "000123456789",
"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:
| Field | Type | Required | Description |
|---|---|---|---|
legal_name | string | Yes | Legal business name |
dba_name | string | Yes | Doing Business As name |
business_type | string | Yes | Entity type: sole_proprietorship, partnership, corporation, llc, non_profit, government, or association_estate_trust. Any other value is rejected with HTTP 422. |
ein | string | Yes | Tax ID / EIN (raw XX-XXXXXXX or VGS token) |
mcc_code | string | No (default: "6513") | 4-digit Merchant Category Code |
website | string | No | Business website URL |
annual_volume | int | No | Estimated annual sales volume in cents |
average_ticket | int | No | Average transaction amount in cents |
high_ticket | int | No | Highest expected transaction in cents |
card_present_percentage | int | No | Card-present percentage (0-100) |
Principal fields:
| Field | Type | Required | Description |
|---|---|---|---|
first_name | string | Yes | First name |
last_name | string | Yes | Last name |
title | string | Yes | Job title (Owner, CEO, etc.) |
email | string | Yes | Email address |
phone | string | Yes | Phone number |
dob | string | No | Date of birth (raw YYYY-MM-DD or VGS token) |
ssn | string | Yes for Clearent | Full SSN (raw XXX-XX-XXXX with optional dashes, or VGS token) — forwarded to processors as Contact.LegalID |
ssn_last4 | string | No | Last 4 digits of SSN — display only, not forwarded to processors |
country_of_citizenship | string | No (default: "US") | Country code |
Banking fields:
| Field | Type | Required | Description |
|---|---|---|---|
routing_number | string | Yes | 9-digit ABA routing number (raw or VGS token) |
account_number | string | Yes | Account number (raw or VGS token) |
account_type | string | No (default: "checking") | checking or savings |
bank_name | string | No | Name of the bank |
Address fields (physical_address and optional mailing_address):
| Field | Type | Required | Description |
|---|---|---|---|
line1 | string | Yes | Street address line 1 |
line2 | string | No | Suite, apt, etc. |
city | string | Yes | City |
state_code | string | Yes | 2-character state code |
zip | string | Yes | ZIP / postal code (5+ chars) |
country_code | string | No (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
retryablefailures (statusdraft) 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
draftto 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.shedpay.us"
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.shedpay.us/widget/paybridge-widget.umd.js"></script>Widget data-* attributes:
| Attribute | Required | Description |
|---|---|---|
data-partner | No | Your referral partner ID for revenue attribution |
data-api-url | Yes | PayBridge API base URL |
data-api-key | Yes | Your consumer app API key |
data-processor-type | No (default: "clearent") | Processor type |
data-vgs-vault-id | Yes | VGS vault ID for secure bank fields |
data-vgs-environment | No (default: "sandbox") | sandbox or live |
data-redirect | No | URL to redirect to after approval |
data-terms-url | No | Terms of Service URL (shown on review step) |
data-privacy-url | No | Privacy Policy URL (shown on review step) |
data-support-email | No | Support email displayed on confirmation screen |
data-status-callback | No | JavaScript 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:
| Field | VGS field name | Validation |
|---|---|---|
| EIN | ein | /^\d{2}-?\d{7}$/ |
| SSN (full) | ssn | /^\d{3}-?\d{2}-?\d{4}$/ |
| Date of birth | dob | /^\d{4}-\d{2}-\d{2}$/ |
| Routing number | routing_number | /^\d{9}$/ |
| Account number | account_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("/boarding/echo", {}, (status, data) => { // the vault forwards here and PayBridge returns the tokens
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:
| Status | Meaning |
|---|---|
draft | A boarding step failed partway. Recoverable — re-submit POST /merchants/{id}/onboard to resume from where it stopped (see Resuming a failed onboarding). |
submitted | Application submitted, awaiting review |
pending_review | Under review by the processor |
approved | Merchant account is active and can process payments |
rejected | Application 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.shedpay.us"
data-widget-token="wt1..."
data-on-uploaded="onDocUploaded"
data-on-error="onDocError">
</div>
<script src="https://api.shedpay.us/widget/paybridge-widget.umd.js"></script>Widget data-* attributes:
| Attribute | Required | Description |
|---|---|---|
data-api-url | Yes | PayBridge API base URL |
data-widget-token | Yes | Short-lived widget token granted the upload_documents action (from POST /widget/sessions) |
data-on-uploaded | No | window-level JS callback name, invoked with { document_id } after a successful upload |
data-on-error | No | window-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
Option A: Embedded Checkout Widget (Recommended)
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.shedpay.us"
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.shedpay.us/widget/paybridge-widget.umd.js"></script>Widget data-* attributes:
| Attribute | Required | Description |
|---|---|---|
data-merchant | Yes | Merchant ID (from Step 1) |
data-amount | Yes | Amount in cents (e.g., 75000 = $750.00) |
data-currency | No (default: "USD") | ISO 4217 currency code |
data-api-url | Yes | PayBridge API base URL |
data-api-key | Yes | Your consumer app API key |
data-vgs-vault-id | Yes | VGS vault ID for secure card/bank fields |
data-vgs-environment | No (default: "sandbox") | sandbox or live |
data-redirect | No | URL to redirect to after successful payment |
data-on-success | No | JavaScript function name on window for success callback |
data-on-error | No | JavaScript 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 IDpay_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, anddata:URIs are blocked. Thedata-redirectURL must be same-origin as the page embedding the widget or match thedata-api-urlorigin.
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"
}
}| Field | Type | Required | Description |
|---|---|---|---|
customer_id | string | Yes | Your own ID for the customer (up to 255 characters). Every method saved with the same ID belongs to the same customer |
card_number_token | string | Yes | VGS token for card number |
cvv_token | string | Yes | VGS token for CVV |
card_holder_token | string | Yes | VGS token for cardholder name |
expires_at | string | Yes | Expiry in YYYY-MM format |
zip | string | Yes | Billing ZIP code |
billing_address | object | No | Full billing address |
last_four | string | No | Last four digits of the card, as VGS Collect's card number field reports them (its last4 state). Send it so the saved card shows its last four where the processor does not report them, as on Aptexx; where the processor does, its value is kept instead |
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"
}| Field | Type | Required | Description |
|---|---|---|---|
customer_id | string | Yes | Your own ID for the customer (up to 255 characters). Every method saved with the same ID belongs to the same customer |
account_holder_token | string | Yes | VGS token for account holder name |
account_number_token | string | Yes | VGS token for account number |
routing_number_token | string | Yes | VGS token for routing number |
account_type | string | No (default: "checking") | checking or savings |
Both save endpoints return the saved method, and its id is the payment_method_id you charge at this merchant. Saving again with a customer_id you have used before adds the new card or bank account to that same customer rather than creating another one.
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"
}
}| Field | Type | Required | Description |
|---|---|---|---|
merchant_id | string | Yes | Merchant ID |
amount | int | Yes | Amount in cents |
currency | string | No (default: "USD") | ISO 4217 currency code |
payment_method_id | string | Yes* | Payment method ID (from add card/bank) |
idempotency_key | string | Yes | Unique key for this request (prevents double-charges on retry). Use your order ID or a UUID. Max 255 chars. |
description | string | No | Payment description |
invoice_number | string | No | Invoice reference |
customer | object | No | Customer info (first_name, last_name, email) |
billing_address | object | No | Billing address |
metadata | object | No | Arbitrary key-value pairs for your records |
customer_email | string | No | Customer email (for portable methods) |
customer_payment_method_id | string | No* | 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).
Expired cards. A card is good through the last day of its expiry month (UTC). Charging a saved card after that, by
payment_method_idorcustomer_payment_method_id, returns422with the codeCARD_EXPIRED, and PayBridge does not contact the processor. Saving a card that has already expired returns the same error. Ask the customer for another card.
Payment statuses:
| Status | Meaning |
|---|---|
posted | Payment completed successfully |
declined | Declined by processor |
failed | Transient processor error |
refunded | Payment 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"
}| Field | Type | Required | Description |
|---|---|---|---|
amount | int | No | Refund amount in cents. Omit for full refund. |
idempotency_key | string | Yes | Unique key for this refund request (prevents duplicate refunds on retry). |
description | string | No | Reason 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.
Option C: Hosted Checkout Sessions (Recommended for Server-to-Server)
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",
"idempotency_key": "order-12345"
}'Response (201):
json
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"checkout_url": "https://api.shedpay.us/checkout/session/f47ac10b-58cc-4372-a567-0e02b2c3d479?st=v1.1710168600.abc123def456...",
"amount": 75000,
"currency": "USD",
"status": "pending",
"expires_at": "2026-03-11T16:00:00",
"created_at": "2026-03-11T15:00:00"
}Fields:
| Field | Type | Required | Description |
|---|---|---|---|
merchant_id | string | Yes | Merchant ID |
amount | int | Yes | Amount in cents (min 1, max 99999999) |
currency | string | No | Default "USD" |
success_url | string | Yes | Where to redirect after successful payment |
cancel_url | string | No | Where to redirect on cancellation |
description | string | No | Payment description (max 500 chars) |
customer_email | string | No | Pre-fill customer email on checkout page |
idempotency_key | string | Yes | Prevents duplicate sessions on retry; resending a key returns the original session |
Security:
success_urlandcancel_urlmust match your app's registeredcors_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)
- Saving a card or bank account to the customer's account, and paying with a card they saved before, after a one-time code emailed to them. Paying with a saved card at a merchant for the first time approves that merchant to charge it
- Payment submission
- Error display and retry
- Redirect to your
success_urlon success orcancel_urlon 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=successStep 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-11T16:00:00",
"completed_at": "2026-03-11T15:05:00"
}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 60 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:00",
"created_at": "2026-03-11T15:00:00"
}
],
"total": 42,
"limit": 20,
"offset": 0
}Query parameters:
| Param | Type | Default | Description |
|---|---|---|---|
status | string | — | Filter by status (pending, posted, declined, refunded, etc.) |
limit | int | 50 | Page size (1–100) |
offset | int | 0 | Number 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:00"
}
]7. Webhooks
PayBridge forwards processor events to your registered webhook URL as standardized JSON payloads.
Event Types
Payment events (forwarded to your webhook URL):
| Event | Trigger |
|---|---|
payment.completed | Payment settled or approved by processor |
payment.declined | Payment declined by processor |
payment.refunded | Refund processed |
Merchant events (forwarded to your webhook URL):
| Event | Trigger |
|---|---|
merchant.approved | Merchant onboarding application approved |
merchant.rejected | Merchant onboarding application rejected |
Checkout events (forwarded to your webhook URL):
| Event | Trigger |
|---|---|
checkout.completed | Hosted 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_idfield 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 within 10 seconds. Delivery is best-effort: a network error or timeout is recorded and retried by the PayBridge team on a backoff schedule, while a non-2xx response counts as delivered and is not retried (see Retry Behavior). For guaranteed delivery, poll the status APIs as a fallback.
Setting Your Webhook URL
Your webhook URL is registered by the PayBridge team. Setting or changing it is an admin-only operation (PUT /admin/apps/{app_id}/webhook-url), so your API key cannot do it: send your integration contact the HTTPS URL you want for each environment — sandbox and production are registered separately. Once it is registered, call POST /webhooks/test with your API key to confirm delivery; the response shows the webhook_url on file.
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 (issued by the PayBridge team with your API key).
Verification steps:
- Read the raw request body bytes (do not parse JSON first).
- Compute HMAC-SHA256 of the body using your
webhook_secret. - Compare your computed signature with the
X-PayBridge-Signatureheader using a constant-time comparison. - 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_idin 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
| Resource | URL |
|---|---|
| API | https://sandbox.api.nfs-pay.com |
| API Docs (Swagger, sandbox only) | https://sandbox.api.nfs-pay.com/docs |
| Widget JS | https://sandbox.api.nfs-pay.com/widget/paybridge-widget.umd.js |
| VGS Environment | sandbox |
Use your sandbox API key for all test requests. Sandbox transactions do not hit real processors.
Test Scenarios
Checkout widget:
- Embed the checkout widget with
data-vgs-environment="sandbox"and your sandbox VGS vault ID. - Fill in test card details in the VGS Collect iframes.
- Verify the success callback fires and the redirect includes
pay_txn_id. - Test error handling: use an invalid amount (0 or negative) to trigger a validation error.
Onboarding widget:
- Embed the onboarding widget with sandbox credentials.
- Walk through all four steps (Business Info, Owner Info, Banking, Review).
- Verify the status polling screen appears after submission.
- Confirm the
data-status-callbackfunction is called with{ status: "submitted", merchantId: "..." }.
API direct:
- Create a merchant via
POST /merchants. - Submit onboarding via
POST /merchants/{id}/onboard. - Poll
GET /merchants/{id}/onboard/statusuntil status changes. - Add a payment method and process a test payment.
- Issue a refund.
Error Codes to Test Against
| Scenario | Expected Error Code | HTTP Status |
|---|---|---|
| Missing API key | VALIDATION_ERROR | 422 |
| Invalid API key | INVALID_API_KEY | 401 |
| Merchant not found | MERCHANT_NOT_FOUND | 404 |
| Invalid request body | VALIDATION_ERROR | 422 |
| Payment declined | PAYMENT_DECLINED | 402 |
| Saved card past its expiry month | CARD_EXPIRED | 422 |
| Processor error | PROCESSOR_ERROR | 502 |
| Rate limit exceeded | RATE_LIMITED | 429 |
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 manages | You configure |
|---|---|
| The VGS vault and account | Load 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 processor | Submit 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):
| Setting | Production value | Notes |
|---|---|---|
| Vault ID | tnt_xxxxxxxxxx | Issued with your production API key |
| Environment | live | Sandbox uses sandbox |
| VGS Collect JS | https://js.verygoodvault.com/vgs-collect/2.24.0/vgs-collect.js | Same SDK in all environments |
| Inbound route (tokenize) | https://tnt_xxxxxxxxxx.live.verygoodproxy.com | VGS 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
| Data | Tokenization |
|---|---|
| Card number, CVV, cardholder name | Required — 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 Merchant Onboarding) |
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:
| Field | VGS Collect / JSON name | Raw format |
|---|---|---|
| EIN | ein | XX-XXXXXXX |
| SSN (full) | ssn | XXX-XX-XXXX (dashes optional) |
| Date of birth | dob | YYYY-MM-DD |
| Routing number | routing_number | 9 digits |
| Account number | account_number | 4–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-urlfromsandbox.api.nfs-pay.comtoapi.shedpay.us. - [ ] CORS origins: Confirm your production domain(s) are registered.
- [ ] Webhook URL: Send your production HTTPS webhook URL to your integration contact to register (see Setting Your Webhook URL), then confirm it with
POST /webhooks/test. - [ ] Webhook handling: Verify your endpoint returns
2xxfor 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-keyattributes.
11. Support
| Channel | Contact |
|---|---|
| Technical support | Reach out to your PayBridge integration contact |
| API status | GET /health (liveness) and GET /ready (full readiness) |
| API documentation | API 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.