Skip to content

Digital Shed Builder — Integration Guide ​

PayBridge API integration guide for the DSB dev team. This covers merchant onboarding and payment processing via the API. PayBridge handles the payment infrastructure — your merchants' card and bank transactions are processed by XplorPay (Clearent), a PCI-certified payment processor. PayBridge manages the integration with the processor on your behalf, including merchant account creation, payment routing, and webhook delivery.

Full field-level details are in the API Reference. Interactive Swagger ({base_url}/docs) and ReDoc ({base_url}/redoc) are served in sandbox only — they are turned off in production.

EnvironmentURL
Sandboxhttps://sandbox.api.nfs-pay.com
Productionhttps://api.shedpay.us

Every example below uses relative paths; prefix them with the base URL of the environment you are calling. Examples that only work in sandbox say so.


Authentication ​

All API requests require your API key in the X-API-Key header:

X-API-Key: your_api_key_here

State-changing requests (POST, PUT, DELETE, PATCH) also require:

X-Requested-With: PayBridge

Overview ​

There are two main workflows:

  1. Merchant Onboarding — Register a merchant, submit their business details to the payment processor (XplorPay/Clearent) for underwriting and approval, and track the approval status. Once approved, the merchant has a live payment processing account.
  2. Checkout — Create a hosted payment session, redirect the customer to a secure PayBridge-hosted payment page (card data is captured there — never touches your servers or ours), and confirm the payment.

Important: All amounts in the API are in minor units (cents). 75000 = $750.00.

┌─────────────────────────────────────────────────────────────┐
│                     MERCHANT ONBOARDING                     │
│                                                             │
│  DSB Backend                 PayBridge       Processor  │
│  ──────────                  ─────────────       ─────────  │
│  POST /merchants ──────────► Create merchant                │
│  POST /merchants/{id}/onboard ──► Submit to ──► Underwrite  │
│  GET  /merchants/{id}/onboard/status ◄── Poll ◄── Approve   │
│  (or receive webhook: merchant.approved)                    │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                        CHECKOUT                             │
│                                                             │
│  DSB Backend                 PayBridge       Customer   │
│  ──────────                  ─────────────       ────────   │
│  POST /checkout/sessions ──► Create session                 │
│  Redirect customer ────────────────────────────► Hosted     │
│                              card/bank form ◄──── payment   │
│                              Process payment                │
│  GET /checkout/sessions/{id} ◄── Confirm                    │
│  (or receive webhook: checkout.completed)                   │
└─────────────────────────────────────────────────────────────┘

Step 1: Register a Merchant ​

When a DSB customer wants to accept payments, create a merchant record.

POST /merchants
json
{
  "name": "Smith Custom Sheds",
  "business_name": "Smith Custom Sheds LLC",
  "business_type": "llc",
  "email": "john@smithsheds.com",
  "phone": "512-555-1234"
}

Response (201):

json
{
  "id": "a1b2c3d4-...",
  "name": "Smith Custom Sheds",
  "business_name": "Smith Custom Sheds LLC",
  "status": "active",
  "processors": [],
  "created_at": "2026-03-26T12:00:00"
}

Save the id — you'll use it for all subsequent calls.


Step 2: Submit Onboarding ​

Submit the merchant's business details to the payment processor for underwriting. The processor (XplorPay/Clearent) reviews the application and decides whether to approve the merchant account.

You collect all merchant details in your own UI and submit them in a single API call. PayBridge forwards everything to the processor's 16-step onboarding API on your behalf.

POST /merchants/{merchant_id}/onboard
X-API-Key: your_api_key
X-Requested-With: PayBridge
Content-Type: application/json
json
{
  "processor_type": "clearent",
  "business": {
    "legal_name": "Smith Custom Sheds LLC",
    "dba_name": "Smith Custom Sheds",
    "business_type": "llc",
    "ein": "27-1234567",
    "mcc_code": "5211",
    "website": "https://smithsheds.com",
    "annual_volume": 50000000,
    "average_ticket": 75000,
    "high_ticket": 250000,
    "card_present_percentage": 0
  },
  "principal": {
    "first_name": "John",
    "last_name": "Smith",
    "title": "Owner",
    "email": "john@smithsheds.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": "000123456789",
    "account_type": "checking",
    "bank_name": "Chase"
  },
  "physical_address": {
    "line1": "123 Workshop Lane",
    "city": "Austin",
    "state_code": "TX",
    "zip": "78701",
    "country_code": "US"
  }
}

Response (202 Accepted):

json
{
  "processor_id": "...",
  "type": "clearent",
  "onboarding_status": "submitted",
  "processor_merchant_id": "CLR_12345",
  "connected": false,
  "resumed": false,
  "submitted_at": "2026-03-26T12:05:00"
}

connected becomes true once the processor account is approved and live credentials are stored. resumed is true when a resubmit continued a previous draft (see below).

Resuming a failed onboarding ​

The 16-step flow runs in one request. If a step fails partway (e.g. a bad routing number) you get HTTP 400, the processor stays in draft status, and the error tells you which step stopped:

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

Fix the offending field and re-submit the same POST /merchants/{id}/onboard. PayBridge resumes from where it stopped — already-completed steps are skipped (no duplicate processor resources, same merchantNumber). A successful resume returns 202 with "resumed": true.

The business identity (legal name, DBA, business type, EIN, owner SSNs) must match the original submission, or you get 409 FROZEN_IDENTITY_CHANGED. A failure at the very first step leaves no draft — just submit again to start fresh. Resuming is Clearent (XplorPay) only.

Sensitive Fields ​

The following fields contain PII/financial data:

FieldExample value
ein27-1234567
ssn123-45-6789
dob1985-06-15
routing_number021000021
account_number000123456789

In sandbox you can submit these as raw values in the request body, exactly as shown above, and complete the full onboarding flow. Production rejects raw values for these fields with HTTP 422 — each one must be a VGS token. Always send them over HTTPS, and never log or persist raw values.

Securing sensitive fields in production ​

Render each sensitive field in a VGS Collect secure iframe on your onboarding page, so the raw value goes from the merchant's browser straight to VGS and only a token comes back. PayBridge runs the vault and detokenizes when it calls the processor; you never configure VGS routes yourself.

SettingValue
VGS Collect JShttps://js.verygoodvault.com/vgs-collect/2.24.0/vgs-collect.js
Vault IDIssued with your production API key (sandbox has its own vault ID)
Environmentlive in production, sandbox in sandbox
Submit path/boarding/echo — the vault forwards to PayBridge, which returns the tokens
Field namesein, ssn, dob, routing_number, account_number
javascript
const collect = VGSCollect.create("tnt_xxxxxxxxxx", "live", () => {}); // your production vault ID
collect.field("#ssn", { type: "text", name: "ssn", validations: ["required", "/^\\d{3}-?\\d{2}-?\\d{4}$/"] });
// ...one field per sensitive value, then:
collect.submit("/boarding/echo", {}, (status, tokens) => {
  // tokens is { ssn: "tok_..." } — put each token in the onboarding body in place of the raw value
});

Send the tokens in exactly the fields shown in the request above, for example "ssn": "tok_...". Add https://js.verygoodvault.com to your page's script-src and frame-src Content-Security-Policy. The full walkthrough is in the Integration Guide's custom onboarding form and Production VGS Setup.

ssn vs ssn_last4: ssn is the full Social Security Number (raw 9 digits in sandbox, or a VGS token in staging/production) and is what gets forwarded to the processor — it is effectively required for Clearent boarding. ssn_last4 is a separate, display-only field that is never forwarded. Send the full ssn; do not rely on ssn_last4 alone.

Required vs Optional Business Fields ​

Field names must match exactly — unknown or misspelled fields are rejected with HTTP 422 (the request bodies use strict validation), not silently ignored.

Required:

FieldDescription
legal_nameLegal business name
dba_name"Doing Business As" name
business_typeOne of llc, corporation, sole_proprietorship, partnership, non_profit, government, association_estate_trust. Any other value is rejected rather than defaulted.
einEmployer Identification Number (XX-XXXXXXX)
first_name / last_name / titlePrincipal owner info
email / phonePrincipal contact info (on the principal object)
routing_number / account_numberSettlement bank account
physical_addressBusiness address (line1, city, state_code, zip)

Optional but recommended (improve approval speed and chances):

FieldDescription
websiteBusiness website URL
annual_volumeExpected annual sales volume (cents)
average_ticketAverage transaction amount (cents)
high_ticketHighest expected transaction (cents)
card_present_percentage0-100, percentage of in-person transactions
mailing_addressMailing address (defaults to physical if omitted)
mcc_codeMerchant Category Code (4 digits). Defaults to 6513 (real estate / rentals) if omitted — set it explicitly for your business.
ownership_percentageEach owner's stake, 0-100 (on principal and each additional_owners entry). Defaults to 100.

Beneficial Owners ​

Clearent underwriting (KYC) requires every individual who owns 25% or more of the business to be disclosed. The principal is the first owner; list any others in an additional_owners array (up to 5 entries), each with the same fields as principal plus an ownership_percentage.

json
{
  "principal": {
    "first_name": "John", "last_name": "Smith", "title": "CEO",
    "email": "john@smithsheds.com", "phone": "512-555-1234",
    "dob": "1985-06-15", "ssn": "123-45-6789",
    "country_of_citizenship": "US",
    "ownership_percentage": 60
  },
  "additional_owners": [
    {
      "first_name": "Jane", "last_name": "Doe", "title": "COO",
      "email": "jane@smithsheds.com", "phone": "512-555-5678",
      "dob": "1983-02-10", "ssn": "987-65-4321",
      "country_of_citizenship": "US",
      "ownership_percentage": 40
    }
  ]
}

Rules (enforced before any processor call — a violation returns HTTP 422):

  • Combined ownership across principal + additional_owners must not exceed 100%.
  • Disclosed owners must total more than 75% — if the disclosed total is 75% or less, PayBridge assumes an undisclosed ≥25% owner is missing and rejects the submission.
  • A single-owner business is fine: leave additional_owners empty and set ownership_percentage to 100 (the default).

Step 3: Track Approval Status ​

In production, processor approval typically takes 1-3 business days while Clearent's underwriting team reviews the application.

The INT sandbox behaves differently, and the differences matter when you are testing against it:

  • Auto-approval only happens when the DBA name contains [APPR]. That tag is Clearent's INT auto-advance trigger. An application without it stays where it is indefinitely — not a fault in your integration, just no trigger.
  • Approval is not instant, and the wait is unpredictable. Observed latency from submit to the first status event ranges from a few seconds to about 45 minutes, with no fixed cycle. Allow the best part of an hour before treating silence as a failure.
  • Once it starts, it finishes quickly. The status events arrive in the order Manual Review → Approved → Boarded, with the remainder landing 1-4 minutes after the first. Boarded is the one that carries the merchant's terminal credentials.
  • Auto-advance is intermittent. Some applications are never advanced and never rejected, with no way to retry or find out why. A stalled application is indistinguishable from a slow one until you have waited.

Because of that last point, do not build a demo or a timed test around INT approving on cue. If a sandbox merchant stalls, ask your integration contact to approve it for you: the PayBridge team can flip a sandbox merchant's processor to approved from the admin side (your API key cannot). That fires the merchant.approved webhook to your app exactly as the processor would, so you can exercise the "payments after approval" half of the flow, and your webhook handler, without waiting on INT.

You have two options for tracking the real status:

Option A: Poll ​

GET /merchants/{merchant_id}/onboard/status
json
{
  "merchant_id": "a1b2c3d4-...",
  "processors": [
    {
      "processor_id": "...",
      "type": "clearent",
      "onboarding_status": "approved",
      "processor_merchant_id": "CLR_12345",
      "submitted_at": "2026-03-26T12:05:00",
      "approved_at": "2026-03-27T14:30:00",
      "rejection_reason": null
    }
  ]
}

Status progression: submitted -> pending_review -> approved or rejected. A mid-flow failure leaves the processor in draft (recoverable — re-submit POST /merchants/{id}/onboard to resume from where it stopped; see Resuming a failed onboarding above).

Option B: Webhook ​

PayBridge sends a signed POST to your webhook URL when the status changes. Your webhook URL is registered by the PayBridge team, not through your API key — see Registering your webhook URL.

json
{
  "event_type": "merchant.approved",
  "webhook_id": "unique-id-for-deduplication",
  "merchant_id": "a1b2c3d4-...",
  "processor_type": "clearent",
  "processor_merchant_id": "CLR_12345",
  "onboarding_status": "approved",
  "timestamp": "2026-03-27T14:30:00.000000+00:00"
}

See the Webhooks section below for signature verification.


Step 4: Create a Checkout Session ​

Once a merchant is approved, you can process payments. When a customer is ready to pay, create a checkout session from your backend:

POST /checkout/sessions
X-API-Key: your_api_key
X-Requested-With: PayBridge
Content-Type: application/json
json
{
  "merchant_id": "a1b2c3d4-...",
  "amount": 75000,
  "currency": "USD",
  "description": "Custom Shed - 10x12 Barn Style",
  "success_url": "https://digitalshedbuilder.com/orders/123/confirmed",
  "cancel_url": "https://digitalshedbuilder.com/orders/123/checkout",
  "customer_email": "buyer@example.com",
  "idempotency_key": "order-123-checkout-v1"
}

Security: The success_url and cancel_url must match origins registered with your consumer app (set during provisioning). This prevents open-redirect attacks.

Response (201) — sandbox shown; the checkout_url host is always the base URL you called:

json
{
  "id": "session-uuid",
  "checkout_url": "https://sandbox.api.nfs-pay.com/checkout/session/session-uuid?st=v1.1711500000.abc123",
  "amount": 75000,
  "currency": "USD",
  "status": "pending",
  "expires_at": "2026-03-26T13:00:00",
  "created_at": "2026-03-26T12:00:00"
}

Step 5: Redirect the Customer ​

Send the customer to the checkout_url from the response. This opens a PayBridge-hosted payment page where the customer enters their card or bank details. Card data is captured on the secure hosted page — it never touches your servers or ours.

javascript
// In your frontend
window.location.href = checkoutSession.checkout_url;

After the customer completes payment, they're redirected to your success_url with query parameters:

https://digitalshedbuilder.com/orders/123/confirmed
  ?pay_session_id=session-uuid
  &pay_payment_id=payment-uuid
  &pay_status=posted
  &pay_amount=75000

If they cancel, they're redirected to your cancel_url.


Step 6: Confirm Payment ​

After the redirect, verify the payment from your backend:

GET /checkout/sessions/{session_id}
json
{
  "id": "session-uuid",
  "status": "completed",
  "amount": 75000,
  "currency": "USD",
  "payment_id": "payment-uuid",
  "completed_at": "2026-03-26T12:02:30"
}

Session statuses:

  • pending — waiting for payment
  • completed — payment succeeded
  • expired — session timed out (60 minutes)

Always verify from your backend — don't trust the redirect query parameters alone.


Webhooks ​

PayBridge sends webhooks for key events. All webhooks include an HMAC-SHA256 signature for verification.

Registering 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.

Events ​

EventWhen
merchant.approvedProcessor approved the onboarding application
merchant.rejectedProcessor rejected the application
checkout.completedHosted checkout payment succeeded
payment.completedPayment settled
payment.declinedPayment declined by processor
payment.refundedRefund processed
merchant.boardedMerchant fully boarded / live at the processor
merchant.pendedApplication pended by the processor
merchant.manual_reviewApplication in manual review (corresponds to pending_review status)

Signature Verification ​

Every webhook includes an X-PayBridge-Signature header (format sha256=<hex>). Verify it by computing an HMAC-SHA256 over the raw request body bytes with your webhook_secret (provided when your app was provisioned) and comparing to the header value. Read the raw received body before any JSON parsing; do not re-serialize the parsed object.

Python:

python
import hashlib
import hmac

def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

# In your webhook handler:
is_valid = verify_webhook(
    raw_body=request.body,
    signature=request.headers["X-PayBridge-Signature"],
    secret="your_webhook_secret",
)

Node.js:

javascript
const crypto = require('crypto');

// Use the raw request body bytes (for example express.raw()), not a re-serialized object.
function verifyWebhook(rawBody, signature, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

checkout.completed Webhook ​

When a customer completes a hosted checkout payment:

json
{
  "event_type": "checkout.completed",
  "webhook_id": "c45de67f-...",
  "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"
}

Retry Behavior ​

A delivery that fails is retried, up to 5 total attempts (1 initial + 4 retries), for every event family — merchant.*, payment.*, checkout.completed, and document.*. Your endpoint should respond within 10 seconds. A retry is triggered only by a network error or timeout — a non-2xx HTTP response is logged and treated as delivered, not retried. Always return 2xx.

Retries are not sent automatically on a timer: the PayBridge team runs them, and each attempt waits at least its backoff window (30 seconds, 5 minutes, 30 minutes, then 2 hours) after the previous failure. A missed event can therefore arrive hours late or not at all, so treat status polling as the authoritative signal. The full schedule is in Webhooks — Retry Behavior.

As a fallback for any missed event, always implement status polling (Step 3 and Step 6).

Deduplication ​

Each webhook includes a webhook_id field. Store processed IDs to prevent handling the same event twice on retries.


Refunds ​

Issue a full or partial refund against a completed payment. Refunds are routed to the same processor that handled the original payment.

POST /payments/{payment_id}/refund
X-API-Key: your_api_key
X-Requested-With: PayBridge
Content-Type: application/json

Full refund (omit amount):

json
{
  "idempotency_key": "refund-order-123-full",
  "description": "Order cancelled by customer"
}

Partial refund:

json
{
  "amount": 25000,
  "idempotency_key": "refund-order-123-partial-v1",
  "description": "Partial refund for returned item"
}

Response (201 Created):

json
{
  "id": "refund-uuid",
  "payment_id": "original-payment-uuid",
  "amount": 25000,
  "status": "posted",
  "description": "Partial refund for returned item",
  "created_at": "2026-03-28T10:00:00"
}

Refund rules:

  • amount is in cents. Omit for full refund.
  • idempotency_key is required — prevents duplicate refunds on retry.
  • Multiple partial refunds are allowed, up to the original payment amount.
  • Only payments in posted or settled status can be refunded.
  • The refund goes to the processor that handled the original payment (including failover scenarios).

Error Handling ​

All API errors return a consistent JSON structure:

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

The envelope always includes a top-level detail string (mirrors error.message). error.details appears only when the error carries structured data (omitted otherwise). Some errors also add error.trace_id (when a request id is present) and error.retryable: true (e.g. 502 PROCESSOR_TRANSIENT_ERROR). Rate-limit 429 and CSRF 403 responses carry only {"error": {"code", "message"}}.

StatusCodeAction
400ONBOARDING_ERROR, BAD_REQUESTCheck error.message for specifics
401INVALID_API_KEY / UNAUTHORIZEDBad or empty X-API-Key. A completely missing header returns 422 instead.
402PAYMENT_DECLINEDShow decline message to customer; they should try another method
403FORBIDDENMissing X-Requested-With: PayBridge header, or request Origin not allowed. (A success_url/cancel_url whose origin isn't registered returns 400 BAD_REQUEST.)
404MERCHANT_NOT_FOUND, TRANSACTION_NOT_FOUND (payment or refund), CHECKOUT_SESSION_NOT_FOUND, DOCUMENT_NOT_FOUND, or RESOURCE_NOT_FOUND (other resources, such as a saved payment method)Check the ID — resource may not exist or may belong to another app
409DUPLICATE_REQUESTIdempotency key already in use by a concurrent request. A repeated key otherwise replays the original result.
422VALIDATION_ERRORRequest body failed validation — check error.details
429RATE_LIMITEDRetry after the Retry-After header period
502PROCESSOR_TRANSIENT_ERRORProcessor temporarily unavailable — safe to retry with same idempotency key

Sandbox Testing ​

The sandbox environment connects to Clearent's sandbox processor. See Testing for the sandbox test cards.

The sandbox base URL is at the top of this document. Your API key will be provided separately.


Rate Limits ​

EndpointLimit
Read operations (GET)100 requests / minute
POST /merchants20 requests / minute
POST /merchants/{id}/onboard20 requests / minute
POST /checkout/sessions20 requests / minute
POST /payments/{id}/refund5 requests / minute
GET /merchants/{id}/onboard/status100 requests / minute
GET /checkout/sessions/{id}100 requests / minute

When rate-limited, you receive HTTP 429 with a Retry-After header indicating seconds to wait.


Production Readiness Checklist ​

Before going live:

  • [ ] API key: Obtain your production API key from the PayBridge team
  • [ ] Base URL: Switch all API calls from sandbox.api.nfs-pay.com to api.shedpay.us
  • [ ] Webhook URL: Send your production HTTPS webhook URL to your integration contact to register, then confirm it with POST /webhooks/test (see Registering your webhook URL)
  • [ ] Webhook verification: Confirm your handler verifies X-PayBridge-Signature on every request
  • [ ] Webhook deduplication: Confirm your handler stores and checks webhook_id to prevent double-processing
  • [ ] Webhook fallback: Implement status polling as a fallback for missed webhooks
  • [ ] CORS origins: Confirm your production domain(s) are registered with PayBridge
  • [ ] Idempotency keys: Confirm all POST requests include unique idempotency_key values (use your order ID or a UUID)
  • [ ] Amounts: Double-check all amounts are in cents. $750.00 = 75000
  • [ ] VGS tokens: Confirm sensitive onboarding fields (ein, full ssn, dob, routing_number, account_number) are submitted as VGS tokens from your production vault ID — required in production (sandbox accepts raw values); see Securing sensitive fields in production
  • [ ] Error handling: Confirm your UI handles PAYMENT_DECLINED, PROCESSOR_TRANSIENT_ERROR, RATE_LIMITED, and network errors gracefully
  • [ ] Refund flow: Test full and partial refunds in sandbox before going live
  • [ ] Merchant onboarding: Complete at least one full onboarding cycle (submit → approved) in sandbox
  • [ ] API key security: Confirm the API key is stored server-side (env var or secrets manager), never in client-side code

Quick Reference ​

# Register merchant
POST /merchants
  -> 201 { id, name, status }

# Submit onboarding
POST /merchants/{id}/onboard
  -> 202 { processor_id, onboarding_status, processor_merchant_id }

# Check approval
GET /merchants/{id}/onboard/status
  -> 200 { merchant_id, processors: [{ onboarding_status, approved_at }] }

# Create checkout session
POST /checkout/sessions
  -> 201 { id, checkout_url, status, expires_at }

# Redirect customer to checkout_url, they pay, redirect back to success_url

# Confirm payment
GET /checkout/sessions/{id}
  -> 200 { id, status, payment_id, completed_at }

# Issue refund
POST /payments/{id}/refund
  -> 201 { id, amount, status }

Support ​

ChannelContact
Technical supportYour PayBridge integration contact
API documentationAPI Reference; interactive Swagger at https://sandbox.api.nfs-pay.com/docs in sandbox only
Webhook documentationWebhooks
Full API referenceAPI Reference