Appearance
PayBridge -- Quick Start Guide
Get from zero to processing payments in under 10 minutes. This guide walks through the complete integration flow with copy-paste code examples.
Prerequisites: You need an API key from the PayBridge team. See the Integration Guide for how to obtain one.
For the current client launch, use the hosted checkout flow in this guide, and onboard merchants through the programmatic API (you collect the details in your own UI and submit them in one call). See the API Reference for the complete field and endpoint reference. The embeddable widgets and merchant portal are out of scope.
The Complete Flow
1. Authenticate --> X-API-Key header on every request
2. Create merchant --> POST /merchants
3. Onboard merchant --> POST /merchants/{id}/onboard
4. Wait for approval --> Poll status OR receive merchant.approved webhook
5. Create checkout --> POST /checkout/sessions
6. Handle payment --> Receive payment.completed webhookStep 1: Authentication
Every API request (except health checks and processor webhooks) requires your API key in the X-API-Key header:
bash
export BASE_URL="https://sandbox.api.nfs-pay.com" # sandbox
export API_KEY="your_api_key_here"All examples below use these environment variables.
Step 2: Create a Merchant
Register the business that will accept payments.
curl
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",
"email": "billing@digitalshedbuilder.com",
"phone": "+15551234567",
"address": {
"street": "100 Commerce Blvd",
"city": "Austin",
"state": "TX",
"zip": "78701",
"country": "US"
}
}'Python
python
import httpx
BASE_URL = "https://sandbox.api.nfs-pay.com"
API_KEY = "your_api_key_here"
HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json", "X-Requested-With": "PayBridge"}
async def create_merchant():
async with httpx.AsyncClient() as client:
response = await client.post(
f"{BASE_URL}/merchants",
headers=HEADERS,
json={
"name": "Digital Shed Builder",
"business_name": "Digital Shed Builder LLC",
"business_type": "llc",
"email": "billing@digitalshedbuilder.com",
"phone": "+15551234567",
"address": {
"street": "100 Commerce Blvd",
"city": "Austin",
"state": "TX",
"zip": "78701",
"country": "US",
},
},
)
response.raise_for_status()
merchant = response.json()
print(f"Merchant created: {merchant['id']}")
return merchantJavaScript
javascript
const BASE_URL = "https://sandbox.api.nfs-pay.com";
const API_KEY = "your_api_key_here";
async function createMerchant() {
const response = await fetch(`${BASE_URL}/merchants`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": API_KEY,
"X-Requested-With": "PayBridge",
},
body: JSON.stringify({
name: "Digital Shed Builder",
business_name: "Digital Shed Builder LLC",
business_type: "llc",
email: "billing@digitalshedbuilder.com",
phone: "+15551234567",
address: {
street: "100 Commerce Blvd",
city: "Austin",
state: "TX",
zip: "78701",
country: "US",
},
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`${response.status}: ${JSON.stringify(error)}`);
}
const merchant = await response.json();
console.log("Merchant created:", merchant.id);
return merchant;
}Response (201):
json
{
"id": "a1b2c3d4-5678-9abc-def0-1234567890ab",
"name": "Digital Shed Builder",
"business_name": "Digital Shed Builder LLC",
"status": "active",
"processors": [],
"created_at": "2026-03-10T12:00:00Z"
}Save the id -- you will need it for all subsequent steps.
Step 3: Onboard the Merchant
Collect the merchant's details in your own UI and submit them in a single call. PayBridge forwards everything to the processor's multi-step onboarding API on your behalf.
bash
MERCHANT_ID="a1b2c3d4-5678-9abc-def0-1234567890ab"
curl -X POST "$BASE_URL/merchants/$MERCHANT_ID/onboard" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-H "X-Requested-With: PayBridge" \
-d '{
"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
},
"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",
"ownership_percentage": 100
},
"banking": {
"routing_number": "021000021",
"account_number": "123456789",
"account_type": "checking",
"bank_name": "Chase"
},
"physical_address": {
"line1": "100 Commerce Blvd",
"city": "Austin",
"state_code": "TX",
"zip": "78701",
"country_code": "US"
}
}'Response (202):
json
{
"processor_id": "p1p2p3p4-...",
"type": "clearent",
"onboarding_status": "submitted",
"processor_merchant_id": "7588000002360113",
"connected": false,
"resumed": false,
"submitted_at": "2026-03-10T12:05:00Z"
}Sensitive values. In sandbox, send ein, dob, ssn, routing_number and account_number as plain values, exactly as shown above — the example runs as-is. Do not substitute placeholder strings that look like tokens (tok_…): 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. Staging and production require these fields to be genuine VGS tokens and reject raw values.
A few values fail in ways that are easy to misread, so prefer the ones above:
| Field | Requirement |
|---|---|
ssn | Full 9 digits, and effectively required — ssn_last4 is display-only and never reaches the processor |
ein | Some values are rejected by the processor's test environment; 27-1234567 is known good |
phone | Full 10-digit US number |
If a step fails you get a 400 naming the failed_step. Fix that field and re-POST the same request — onboarding resumes from where it stopped rather than starting over.
Alternative: hosted onboarding
POST /merchants/{id}/onboard/hosted returns an application_url that the merchant completes themselves on the processor's site. It is not the supported path for this integration — it gives up the programmatic control and attribution the API exists to provide. See Hosted Merchant Onboarding if you have a specific reason to use it.
Step 4: Wait for Approval
There are two ways to know when the merchant is approved:
Option A: Poll the Status Endpoint
bash
curl "$BASE_URL/merchants/$MERCHANT_ID/onboard/status" \
-H "X-API-Key: $API_KEY"The 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.
json
{
"merchant_id": "a1b2c3d4-...",
"processors": [
{
"processor_id": "p1p2p3p4-...",
"type": "clearent",
"onboarding_status": "approved",
"processor_merchant_id": "CLR_12345",
"submitted_at": "2026-03-10T12:05:00Z",
"approved_at": "2026-03-10T14:30:00Z"
}
]
}Python polling example:
python
import asyncio
import httpx
async def wait_for_approval(merchant_id: str, timeout_minutes: int = 60):
"""Poll onboarding status until approved or timeout."""
async with httpx.AsyncClient() as client:
deadline = asyncio.get_event_loop().time() + (timeout_minutes * 60)
while asyncio.get_event_loop().time() < deadline:
response = await client.get(
f"{BASE_URL}/merchants/{merchant_id}/onboard/status",
headers=HEADERS,
)
response.raise_for_status()
data = response.json()
for proc in data["processors"]:
if proc["onboarding_status"] == "approved":
print(f"Merchant approved! Processor: {proc['type']}")
return proc
elif proc["onboarding_status"] == "rejected":
raise Exception(f"Merchant rejected by {proc['type']}")
print("Still pending... checking again in 30 seconds")
await asyncio.sleep(30)
raise TimeoutError("Onboarding approval timed out")Option B: Receive the merchant.approved Webhook
If you have a webhook URL configured, PayBridge will send a merchant.approved event when the processor approves the merchant. See the Webhook Documentation for details.
json
{
"event_type": "merchant.approved",
"webhook_id": "a23bc45d-...",
"merchant_id": "a1b2c3d4-...",
"processor_type": "clearent",
"processor_merchant_id": "CLR_12345",
"onboarding_status": "approved",
"timestamp": "2026-03-10T14:30:00.000000+00:00"
}Step 5: Create a Checkout Session
Once the merchant is approved, you can accept payments. The easiest way is hosted checkout sessions -- your backend creates a session, then redirects the customer.
curl
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://digitalshedbuilder.com/payment-success",
"cancel_url": "https://digitalshedbuilder.com/payment-cancelled",
"description": "10x12 Shed - Order #1042",
"customer_email": "jane@example.com",
"idempotency_key": "sess-order-1042"
}'Python
python
async def create_checkout_session(merchant_id: str, order_id: str, amount_cents: int):
"""Create a hosted checkout session and return the checkout URL."""
async with httpx.AsyncClient() as client:
response = await client.post(
f"{BASE_URL}/checkout/sessions",
headers=HEADERS,
json={
"merchant_id": merchant_id,
"amount": amount_cents,
"currency": "USD",
"success_url": f"https://digitalshedbuilder.com/orders/{order_id}/success",
"cancel_url": f"https://digitalshedbuilder.com/orders/{order_id}/cancelled",
"description": f"Order #{order_id}",
"customer_email": "jane@example.com",
"idempotency_key": f"sess-order-{order_id}",
},
)
response.raise_for_status()
session = response.json()
print(f"Checkout URL: {session['checkout_url']}")
return sessionJavaScript
javascript
async function createCheckoutSession(merchantId, orderId, amountCents) {
const response = await fetch(`${BASE_URL}/checkout/sessions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": API_KEY,
"X-Requested-With": "PayBridge",
},
body: JSON.stringify({
merchant_id: merchantId,
amount: amountCents,
currency: "USD",
success_url: `https://digitalshedbuilder.com/orders/${orderId}/success`,
cancel_url: `https://digitalshedbuilder.com/orders/${orderId}/cancelled`,
description: `Order #${orderId}`,
customer_email: "jane@example.com",
idempotency_key: `sess-order-${orderId}`,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`${response.status}: ${JSON.stringify(error)}`);
}
const session = await response.json();
console.log("Checkout URL:", session.checkout_url);
return session;
}
// Redirect the customer to the checkout page
// window.location.href = session.checkout_url;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-10T12:30:00Z",
"created_at": "2026-03-10T12:00:00Z"
}Redirect your customer to checkout_url. The hosted page handles VGS tokenization, payment processing, and redirects back to your success_url when done.
After the redirect, verify the payment by polling the session status:
bash
curl "$BASE_URL/checkout/sessions/$SESSION_ID" \
-H "X-API-Key: $API_KEY"json
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"status": "completed",
"amount": 75000,
"payment_id": "txn_abc123",
"completed_at": "2026-03-10T12:05:00Z"
}Step 6: Handle the Payment Webhook
When the payment processor settles the transaction, PayBridge sends a payment.completed webhook to your registered URL.
json
{
"event_type": "payment.completed",
"webhook_id": "f47ac10b-...",
"payment_id": "txn_abc123",
"merchant_id": "a1b2c3d4-...",
"amount": 75000,
"status": "posted",
"timestamp": "2026-03-10T12:15:30.123456+00:00"
}Minimal Webhook Handler (Python)
python
import hashlib
import hmac
import json
from fastapi import FastAPI, Header, HTTPException, Request
app = FastAPI()
WEBHOOK_SECRET = "your-webhook-secret" # from POST /admin/apps response
def verify_signature(body: bytes, signature: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.post("/api/pb-webhooks")
async def handle_webhook(
request: Request,
x_paybridge_signature: str = Header("", alias="X-PayBridge-Signature"),
):
body = await request.body()
# 1. Verify signature
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["event_type"]
webhook_id = payload.get("webhook_id")
# 2. Deduplicate (implement your own storage)
# if already_processed(webhook_id):
# return {"status": "ok"}
# 3. Handle the event
if event_type == "payment.completed":
# Mark order as paid, send receipt, start fulfillment
print(f"Payment {payload['payment_id']} completed: "
f"${payload['amount'] / 100:.2f}")
elif event_type == "payment.declined":
# Notify customer, update order status
print(f"Payment {payload['payment_id']} declined")
elif event_type == "payment.refunded":
# Update order, notify customer
print(f"Refund on {payload['payment_id']}: "
f"${abs(payload['amount']) / 100:.2f}")
elif event_type == "merchant.approved":
# Enable payment processing for this merchant
print(f"Merchant {payload['merchant_id']} approved")
elif event_type == "merchant.rejected":
print(f"Merchant {payload['merchant_id']} rejected")
# 4. Acknowledge receipt
return {"status": "ok"}Error Handling Patterns
Handling API Errors
All errors follow a consistent structure:
json
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable description",
"details": {}
}
}Python Error Handling
python
import httpx
class PayBridgeError(Exception):
def __init__(self, status_code: int, code: str, message: str, details: dict):
self.status_code = status_code
self.code = code
self.message = message
self.details = details
super().__init__(f"{code}: {message}")
async def api_request(method: str, path: str, **kwargs):
"""Make an API request with proper error handling."""
async with httpx.AsyncClient() as client:
response = await client.request(
method,
f"{BASE_URL}{path}",
headers=HEADERS,
**kwargs,
)
if response.status_code >= 400:
body = response.json()
error = body.get("error", {})
raise PayBridgeError(
status_code=response.status_code,
code=error.get("code", "UNKNOWN"),
message=error.get("message", "Unknown error"),
details=error.get("details", {}),
)
return response.json()
# Usage with error handling
async def process_payment_safely(merchant_id, amount, method_id, idempotency_key):
try:
result = await api_request(
"POST",
"/payments",
json={
"merchant_id": merchant_id,
"amount": amount,
"payment_method_id": method_id,
"idempotency_key": idempotency_key,
},
)
print(f"Payment succeeded: {result['id']}")
return result
except PayBridgeError as e:
if e.code == "PAYMENT_DECLINED":
reason = e.details.get("decline_reason", "unknown")
print(f"Payment declined: {reason}")
# Notify customer to try a different payment method
elif e.code == "RATE_LIMITED":
print("Rate limited -- retry after delay")
# Implement exponential backoff
elif e.code == "PROCESSOR_TRANSIENT_ERROR":
print("Processor temporarily unavailable")
# Safe to retry with the same idempotency_key
else:
print(f"Unexpected error: {e.code} - {e.message}")
raiseJavaScript Error Handling
javascript
class PayBridgeError extends Error {
constructor(statusCode, code, message, details) {
super(`${code}: ${message}`);
this.statusCode = statusCode;
this.code = code;
this.details = details;
}
}
async function apiRequest(method, path, body = null) {
const options = {
method,
headers: {
"Content-Type": "application/json",
"X-API-Key": API_KEY,
"X-Requested-With": "PayBridge",
},
};
if (body) options.body = JSON.stringify(body);
const response = await fetch(`${BASE_URL}${path}`, options);
if (!response.ok) {
const data = await response.json();
const error = data.error || {};
throw new PayBridgeError(
response.status,
error.code || "UNKNOWN",
error.message || "Unknown error",
error.details || {}
);
}
return response.json();
}
// Usage
try {
const payment = await apiRequest("POST", "/payments", {
merchant_id: merchantId,
amount: 75000,
payment_method_id: methodId,
idempotency_key: `order-${orderId}-v1`,
});
console.log("Payment succeeded:", payment.id);
} catch (e) {
if (e instanceof PayBridgeError) {
switch (e.code) {
case "PAYMENT_DECLINED":
console.log("Declined:", e.details.decline_reason);
break;
case "RATE_LIMITED":
console.log("Rate limited -- retry later");
break;
default:
console.error("API error:", e.code, e.message);
}
} else {
console.error("Network error:", e.message);
}
}Alternative: Direct API Payment Flow
If you are not using hosted checkout sessions and want to manage the full payment flow yourself:
1. Add a Payment Method
bash
# Add a card (tokens come from VGS Collect JS in the browser)
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"
}'2. 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,
"payment_method_id": "pm_xyz789",
"idempotency_key": "order-1042-v1",
"description": "10x12 Shed - Order #1042"
}'3. Issue a Refund (if needed)
bash
# Partial refund (amount in cents)
curl -X POST "$BASE_URL/payments/$PAYMENT_ID/refund" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-H "X-Requested-With: PayBridge" \
-d '{
"amount": 25000,
"idempotency_key": "refund-order-1042-v1",
"description": "Customer requested partial refund"
}'
# Full refund (omit amount)
curl -X POST "$BASE_URL/payments/$PAYMENT_ID/refund" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-H "X-Requested-With: PayBridge" \
-d '{
"idempotency_key": "refund-order-1042-full",
"description": "Order cancelled"
}'Key Concepts to Remember
| Concept | Detail |
|---|---|
| Amounts | Always in minor units (cents). 75000 = $750.00. |
| Idempotency | Required on payments and refunds. Use your order ID or a UUID. Safe to retry with the same key. |
| VGS tokens | Card numbers, CVVs, bank accounts, EINs, SSNs, and DOBs are tokenized by VGS. Your servers never see raw PCI/PII data. |
| Failover | Automatic retry on secondary processor for transient errors. Terminal declines are not retried. |
| Tenant isolation | All resources are scoped to your API key. You cannot see or modify another app's merchants/payments. |
| Webhooks | Always verify the X-PayBridge-Signature header. Always deduplicate using webhook_id. |
Next Steps
- Full API Reference -- every endpoint, field, and error code
- Webhook Documentation -- event types, signature verification, retry behavior
- Integration Guide -- widget embedding, VGS Collect, onboarding flows
- Widget Embedding -- checkout and onboarding widget setup