> ## Agent Instructions > Use hosted checkout for a new payment integration unless the user explicitly needs an SDK or Direct QR flow. > Keep secret API keys on the server. Never place an sk_test_ or sk_live_ key in browser or mobile code. > Never treat a browser return URL as proof of payment. Verify a signed webhook and confirm status, amount, and currency on the server before fulfillment. > Fonepay has no sandbox. A Fonepay payment made from a sandbox project uses the merchant's real Fonepay account and moves real money within the documented caps. > Do not recommend direct integration with Mobile SDK device endpoints. Use the PayBridgeNP Mobile SDK and the merchant-facing session creation API. # Get account Source: https://docs.paybridgenp.com/api-reference/account/get-account GET /v1/account Returns the merchant, project, plan, and API-key context for the calling API key. Use it to confirm which project, mode, and permissions a key represents. Requires the `account:read` scope. The merchant email is masked (e.g. `aa***@example.com`) unless the key also holds `pii:read`. # Analytics overview Source: https://docs.paybridgenp.com/api-reference/analytics/overview GET /v1/analytics/overview Returns aggregated KPIs for the authenticated project over a rolling window - payment totals, success rate, provider breakdown, and the checkout funnel. Requires the `analytics:read` scope. # Authentication Source: https://docs.paybridgenp.com/api-reference/authentication How to authenticate with the PayBridgeNP API using Bearer API keys. All merchant-facing PayBridgeNP API endpoints require a Bearer API key in the `Authorization` header: ```http theme={null} Authorization: Bearer sk_test_your_key_here ``` ## Getting your API keys Sign up at [dashboard.paybridgenp.com](https://dashboard.paybridgenp.com) and go to **Settings → API Keys**. ## Key prefixes | Prefix | Mode | Use | | ---------- | ------- | ---------------------------------------------------------- | | `sk_test_` | Sandbox | eSewa and Khalti test flows; see the Fonepay warning below | | `sk_live_` | Live | Production - real payments | The mode is fixed per key. A sandbox key creates sandbox records; a live key creates live payments. Fonepay has no sandbox. If Fonepay is enabled on a sandbox project, it uses your real Fonepay merchant account and moves real money. The sandbox caps are NPR 1,000 per payment and NPR 5,000 per month. See [Sandbox testing](/guides/sandbox-testing). Never expose a secret API key in client-side code, browser JavaScript, or version control. Use server-side environment variables only. ## Example request ```bash theme={null} curl https://api.paybridgenp.com/v1/payments \ -H "Authorization: Bearer sk_test_your_key" ``` ## Authentication errors When authentication fails, the response body always includes a `code` field so your integration can branch on the failure mode without parsing human-readable messages. | `code` | HTTP | When it fires | | ------------------------ | ---- | -------------------------------------------------------------------------------- | | `api_key_missing` | 401 | No `Authorization: Bearer ...` header, or the key portion is empty. | | `api_key_invalid` | 401 | Key not recognized - typo, deleted project, or wrong environment. | | `api_key_revoked` | 401 | Key was explicitly revoked from the dashboard. Generate a new one. | | `api_key_expired` | 401 | Key passed its expiration timestamp. | | `api_key_must_be_secret` | 403 | Endpoint requires `sk_...`; a publishable `pk_...` key was sent. | | `account_suspended` | 403 | Merchant account suspended. See `suspended_at` + `suspended_reason` in the body. | Example invalid-key response: ```json theme={null} { "error": { "message": "Invalid API key. Verify the key in your dashboard at https://dashboard.paybridgenp.com.", "type": "authentication_error", "code": "api_key_invalid", "request_id": "req_2Je91NlWKuXkdXUJOK9gaHNW" } } ``` ## Account suspended If your merchant account has been suspended by PayBridgeNP, all API key requests return: ```json theme={null} { "error": { "message": "This merchant account has been suspended. Contact support@paybridgenp.com to resolve.", "type": "account_error", "code": "account_suspended", "request_id": "req_...", "suspension": { "suspended_at": "2026-04-20T10:00:00.000Z", "reason": "Violation of terms of service" } } } ``` HTTP status: `403 Forbidden` Your API keys, checkout sessions, and hosted payment pages will all be blocked until the suspension is lifted. Contact [support@paybridgenp.com](mailto:support@paybridgenp.com) to resolve. ## Rotating keys You can generate a new API key from the dashboard at any time. Old keys remain valid until you revoke them. Revoke a key immediately if it's been exposed. # Create a coupon Source: https://docs.paybridgenp.com/api-reference/billing/coupons/create-coupon POST /v1/billing/coupons Creates a reusable discount. Live mode requires Growth or higher; sandbox is available on every plan. Discount parameters are immutable after creation; only `active` can change through PATCH. # Deactivate a coupon Source: https://docs.paybridgenp.com/api-reference/billing/coupons/deactivate-coupon DELETE /v1/billing/coupons/{id} Alias for PATCH `{ active: false }`. Coupons are never hard-deleted (redemption history is preserved). # Get a coupon Source: https://docs.paybridgenp.com/api-reference/billing/coupons/get-coupon GET /v1/billing/coupons/{id} Retrieves a single coupon by ID. # List coupons Source: https://docs.paybridgenp.com/api-reference/billing/coupons/list-coupons GET /v1/billing/coupons Returns the merchant's coupons, optionally filtered by active status. # Update a coupon Source: https://docs.paybridgenp.com/api-reference/billing/coupons/update-coupon PATCH /v1/billing/coupons/{id} Only `{ active: false }` is supported; discount parameters are immutable. Deactivating stops future redemptions. # Adjust a customer's credit balance Source: https://docs.paybridgenp.com/api-reference/billing/customers/add-credit POST /v1/billing/customers/{id}/credit Adds (positive) or deducts (negative) account credit in paisa. Credit is automatically applied to future invoices. Returns the updated customer. # Create a customer Source: https://docs.paybridgenp.com/api-reference/billing/customers/create-customer POST /v1/billing/customers Creates a billing customer. Customers are required to create subscriptions. # Delete a customer Source: https://docs.paybridgenp.com/api-reference/billing/customers/delete-customer DELETE /v1/billing/customers/{id} Deletes a customer. Will return `409 Conflict` if the customer has active, paused, or past-due subscriptions - cancel those first. # Get a customer Source: https://docs.paybridgenp.com/api-reference/billing/customers/get-customer GET /v1/billing/customers/{id} Retrieves a single billing customer by ID. # List customers Source: https://docs.paybridgenp.com/api-reference/billing/customers/list-customers GET /v1/billing/customers Returns a paginated list of billing customers. Accepts a `search` query that matches on name, email, phone, or external customer ID. # Update a customer Source: https://docs.paybridgenp.com/api-reference/billing/customers/update-customer PATCH /v1/billing/customers/{id} Updates a billing customer's name, contact details, external ID, or metadata. A customer must always keep at least one of email or phone. # Assign a dunning policy to a subscription Source: https://docs.paybridgenp.com/api-reference/billing/dunning/assign-policy POST /v1/billing/dunning/subscriptions/{id}/policy Assigns (or clears, with `policyId: null`) the dunning policy for a subscription. # Create a dunning policy Source: https://docs.paybridgenp.com/api-reference/billing/dunning/create-policy POST /v1/billing/dunning/policies Creates a dunning policy that defines the retry schedule and final action for failed invoice payments. Returns 404 when dunning is not enabled. # Get invoice dunning status Source: https://docs.paybridgenp.com/api-reference/billing/dunning/get-invoice-dunning GET /v1/billing/dunning/invoices/{id}/dunning Returns the current dunning state for an invoice, including retry attempts and the next scheduled retry. # Get a dunning policy Source: https://docs.paybridgenp.com/api-reference/billing/dunning/get-policy GET /v1/billing/dunning/policies/{id} Retrieves a single dunning policy by ID. # List dunning policies Source: https://docs.paybridgenp.com/api-reference/billing/dunning/list-policies GET /v1/billing/dunning/policies Dunning policies define the retry schedule for overdue invoices. Live mode requires Growth or higher; sandbox is available on every plan. # Retry dunning now Source: https://docs.paybridgenp.com/api-reference/billing/dunning/retry-now POST /v1/billing/dunning/invoices/{id}/dunning/retry-now Triggers an immediate dunning retry for an invoice currently in the `retrying` state. # Stop dunning Source: https://docs.paybridgenp.com/api-reference/billing/dunning/stop-dunning POST /v1/billing/dunning/invoices/{id}/dunning/stop Stops the dunning cycle for an invoice (sets status to `stopped`). # Update a dunning policy Source: https://docs.paybridgenp.com/api-reference/billing/dunning/update-policy PATCH /v1/billing/dunning/policies/{id} Updates a dunning policy's name, retry schedule, final action, or default flag. # Pay an invoice with a Fonepay Direct-QR Source: https://docs.paybridgenp.com/api-reference/billing/invoices/create-qr POST /v1/billing/invoices/{id}/qr Mint a **Fonepay Direct-QR** for a specific invoice so the customer can pay it by scanning a QR in your own UI / at a counter, instead of the hosted bill page. A successful scan marks the invoice paid and **activates the subscription** (`incomplete`→`active`) - the same outcome as paying via the bill page. No request body - the amount and customer come from the invoice. The returned session also works with [Stream Direct-QR events](#tag/QR) (`GET /v1/qr/{id}/events`, SSE) and [Refresh a Fonepay Direct-QR](#tag/QR) (`POST /v1/qr/{id}/refresh`). Requires the `billing:write` scope (available on Growth and higher) and Fonepay configured for the project in this mode. # Get an invoice Source: https://docs.paybridgenp.com/api-reference/billing/invoices/get-invoice GET /v1/billing/invoices/{id} Retrieves a single invoice by ID, including its customer and line items. # List invoices Source: https://docs.paybridgenp.com/api-reference/billing/invoices/list-invoices GET /v1/billing/invoices Returns a paginated list of billing invoices, ordered by most recent first. # Create a billing plan Source: https://docs.paybridgenp.com/api-reference/billing/plans/create-plan POST /v1/billing/plans Creates a recurring billing plan. Subscriptions are tied to a plan and inherit its amount, interval, and dunning settings. **Amount and interval are immutable after creation.** To change them, create a new plan and migrate subscribers using the Change Plan endpoint. # Get a billing plan Source: https://docs.paybridgenp.com/api-reference/billing/plans/get-plan GET /v1/billing/plans/{id} Retrieves a single billing plan by ID. # List billing plans Source: https://docs.paybridgenp.com/api-reference/billing/plans/list-plans GET /v1/billing/plans Returns a paginated list of billing plans. # Update a billing plan Source: https://docs.paybridgenp.com/api-reference/billing/plans/update-plan PATCH /v1/billing/plans/{id} Updates plan metadata, active status, or dunning settings. Amount and interval cannot be changed. # Create a promotion code Source: https://docs.paybridgenp.com/api-reference/billing/promotion-codes/create-promotion-code POST /v1/billing/promotion-codes Creates a customer-facing code that applies a coupon's discount. Live mode requires Growth or higher; sandbox is available on every plan. # Deactivate a promotion code Source: https://docs.paybridgenp.com/api-reference/billing/promotion-codes/deactivate-promotion-code PATCH /v1/billing/promotion-codes/{id} Only `{ active: false }` is supported. Deactivating stops future redemptions. # Get a promotion code Source: https://docs.paybridgenp.com/api-reference/billing/promotion-codes/get-promotion-code GET /v1/billing/promotion-codes/{id} Retrieves a single promotion code by ID. # List promotion codes Source: https://docs.paybridgenp.com/api-reference/billing/promotion-codes/list-promotion-codes GET /v1/billing/promotion-codes Returns promotion codes for the merchant, optionally filtered by `couponId` and active status. # Validate a promotion code Source: https://docs.paybridgenp.com/api-reference/billing/promotion-codes/validate-promotion-code POST /v1/billing/promotion-codes/validate Checks whether a code is redeemable (active, not expired, redemption + customer + minimum-amount restrictions) and previews the discount. Does not consume a redemption. # Get tax settings Source: https://docs.paybridgenp.com/api-reference/billing/settings/get-tax GET /v1/billing/settings/tax Returns the account-level tax configuration applied to invoices. # Update tax settings Source: https://docs.paybridgenp.com/api-reference/billing/settings/tax PATCH /v1/billing/settings/tax Updates the merchant's tax settings: enabled flag, rate in basis points, registration number, and label. These apply to the merchant's invoices across every project. # Apply a coupon to a subscription Source: https://docs.paybridgenp.com/api-reference/billing/subscriptions/apply-coupon POST /v1/billing/subscriptions/{id}/apply-coupon Attaches a coupon (directly or via a promotion code) to a subscription. One active discount per subscription. # Cancel a subscription Source: https://docs.paybridgenp.com/api-reference/billing/subscriptions/cancel-subscription POST /v1/billing/subscriptions/{id}/cancel Cancels a subscription. - **Immediate** (default): cancels now, no further invoices generated. - **At period end** (`atPeriodEnd: true`): subscription stays active until the current billing period ends, then cancels automatically. # Change subscription plan Source: https://docs.paybridgenp.com/api-reference/billing/subscriptions/change-plan POST /v1/billing/subscriptions/{id}/change-plan Schedules a plan change for the next billing period. The current period continues on the old plan. At renewal, the subscription switches to the new plan and generates an invoice at the new amount. # Add a pending invoice item Source: https://docs.paybridgenp.com/api-reference/billing/subscriptions/create-invoice-item POST /v1/billing/subscriptions/{id}/invoice-items Queues a one-off charge onto the subscription's next invoice. # Create a subscription Source: https://docs.paybridgenp.com/api-reference/billing/subscriptions/create-subscription POST /v1/billing/subscriptions Creates a subscription for a customer on a billing plan. PayBridgeNP immediately generates the first invoice and sends a payment link to the customer. If the plan has `trialDays > 0`, the subscription starts in trial - no invoice is generated until the trial ends. # Delete a pending invoice item Source: https://docs.paybridgenp.com/api-reference/billing/subscriptions/delete-invoice-item DELETE /v1/billing/subscriptions/{id}/invoice-items/{itemId} Deletes a pending one-off invoice item before it is billed onto the next invoice. Responds successfully even if the item no longer exists. # End a trial immediately Source: https://docs.paybridgenp.com/api-reference/billing/subscriptions/end-trial POST /v1/billing/subscriptions/{id}/end-trial Ends the trial now and generates the first paid invoice. Idempotent - repeated calls return 409 once the trial has ended. # Extend a trial Source: https://docs.paybridgenp.com/api-reference/billing/subscriptions/extend-trial POST /v1/billing/subscriptions/{id}/extend-trial Moves the trial end date later. The new date must be in the future. # Get a subscription Source: https://docs.paybridgenp.com/api-reference/billing/subscriptions/get-subscription GET /v1/billing/subscriptions/{id} Retrieves a single subscription by ID, including its computed display status and current billing period. # Get usage summary Source: https://docs.paybridgenp.com/api-reference/billing/subscriptions/get-usage-summary GET /v1/billing/subscriptions/{id}/usage Returns the aggregated usage for the current period of a metered subscription. # List pending invoice items Source: https://docs.paybridgenp.com/api-reference/billing/subscriptions/list-invoice-items GET /v1/billing/subscriptions/{id}/invoice-items One-off charges queued onto the subscription's next invoice. # List subscriptions Source: https://docs.paybridgenp.com/api-reference/billing/subscriptions/list-subscriptions GET /v1/billing/subscriptions Returns a paginated list of subscriptions. Supports filtering by display status, customer ID, and plan ID. # List usage records Source: https://docs.paybridgenp.com/api-reference/billing/subscriptions/list-usage-records GET /v1/billing/subscriptions/{id}/usage/records Returns recent usage records reported against a metered subscription, most recent first. # Pause a subscription Source: https://docs.paybridgenp.com/api-reference/billing/subscriptions/pause-subscription POST /v1/billing/subscriptions/{id}/pause Pauses an active subscription. No invoices are generated while paused. Resume with the `/resume` endpoint. # Preview proration for a plan change Source: https://docs.paybridgenp.com/api-reference/billing/subscriptions/preview-proration GET /v1/billing/subscriptions/{id}/preview-proration Computes the credit/debit for switching to a different plan now, without applying it. # Remove a subscription's discount Source: https://docs.paybridgenp.com/api-reference/billing/subscriptions/remove-discount DELETE /v1/billing/subscriptions/{id}/discount Removes the active coupon discount from a subscription. Returns 404 when no discount is currently applied. # Report usage Source: https://docs.paybridgenp.com/api-reference/billing/subscriptions/report-usage POST /v1/billing/subscriptions/{id}/usage Records a usage event for a metered subscription. Use `idempotency_key` to safely retry without double-counting. # Resume a subscription Source: https://docs.paybridgenp.com/api-reference/billing/subscriptions/resume-subscription POST /v1/billing/subscriptions/{id}/resume Resumes a paused subscription. A new invoice is generated for the current billing period. # Update subscription quantity Source: https://docs.paybridgenp.com/api-reference/billing/subscriptions/update-quantity PATCH /v1/billing/subscriptions/{id}/quantity Changes the seat/unit quantity. Only on active, trialing, or past_due subscriptions. # Create a checkout session Source: https://docs.paybridgenp.com/api-reference/checkout/create-session POST /v1/checkout Creates a new checkout session and returns a `checkout_url` to redirect your customer to. Use `hosted` (default) to let the customer choose a provider, or `redirect` with a provider to skip the picker. The browser return is navigation only; verify payment status, amount, and currency on your server before fulfillment. **Idempotency:** Pass an `Idempotency-Key` header to safely retry failed requests without creating duplicate sessions. The key is scoped to your project and expires after 24 hours. # Expire a checkout session Source: https://docs.paybridgenp.com/api-reference/checkout/expire-session POST /v1/checkout/{id}/expire Marks a checkout session as expired so it can no longer accept payment. Use this when you mint a fresh checkout session for a logical purchase that already had one outstanding (for example, a customer requested a new payment link after the original expired). Without explicitly expiring the old session, its URL remains payable until the session's natural 30-minute TTL elapses, which can let a customer who reloads the old tab pay twice. **Idempotent.** Calling expire on a session that is already in a terminal state (`success`, `failed`, `cancelled`, `expired`) returns 200 with the current status - no error. Only `pending` and `initiated` sessions are flipped. If the session has an active eSewa Intent booking, PayBridgeNP first confirms that booking is paid or dead at eSewa. When eSewa cannot confirm cancellation, the call returns 409 with `code: "provider_cancel_unconfirmed"` and leaves the session observed; retry before minting another payable session. # Get a checkout session Source: https://docs.paybridgenp.com/api-reference/checkout/get-session GET /v1/sessions/{id} Retrieves a single checkout session by ID. Once the session is paid, the response includes `paymentId` - pass it to `GET /v1/payments/{id}` for full payment details. Requires the `sessions:read` scope. # List checkout sessions Source: https://docs.paybridgenp.com/api-reference/checkout/list-sessions GET /v1/sessions Returns checkout sessions for the authenticated project, newest first. Read-only - sessions are created via `POST /v1/checkout`. Requires the `sessions:read` scope. # Errors Source: https://docs.paybridgenp.com/api-reference/errors HTTP status codes, error envelope shape, and codes returned by the PayBridgeNP API. All errors return a single nested envelope. The flat `{error, code}` shape used in earlier docs has been replaced. See the [migration note](#migrating-from-the-flat-envelope) below if you have older code. ```json theme={null} { "error": { "message": "Invalid API key. Verify the key in your dashboard at https://dashboard.paybridgenp.com.", "type": "authentication_error", "code": "api_key_invalid", "request_id": "req_2Je91NlWKuXkdXUJOK9gaHNW" } } ``` ## Fields | Field | Type | Description | | ------------------ | ------------- | ---------------------------------------------------------------------------------------------------------------- | | `error.message` | string | Human-readable explanation. Safe to surface in logs; safe to show developers. Not always safe to show end users. | | `error.type` | string (enum) | Broad category - SDKs use this to instantiate typed exceptions. See [error types](#error-types). | | `error.code` | string | Specific, stable identifier for programmatic branching. See [error codes](#error-codes). | | `error.request_id` | string | Same value as the `X-Request-Id` response header. Quote in support requests for fastest triage. | | `error.suspension` | object | Present only on `code: "account_suspended"` - includes `suspended_at` and `reason`. | | `error.pause` | object | Present only on `code: "token_paused"` - includes `paused_at` and `reason`. | ## Error types Use `error.type` to route an error to the right handler. Each type maps to a typed exception class in the SDKs. | Type | Status range | Meaning | | ----------------------- | ------------------ | -------------------------------------------------------------------------------------------- | | `authentication_error` | 401 | Auth credentials missing or invalid | | `account_error` | 403, 410, 423 | Credentials valid but the account or token is not in good standing | | `permission_error` | 402, 403 | Action not permitted (missing scope, wrong key type, plan gate, or a quota/entitlement wall) | | `invalid_request_error` | 400, 404, 409, 422 | Request was malformed, references something that does not exist, or violates business rules | | `idempotency_error` | 409 | Concurrent request with same `Idempotency-Key` | | `rate_limit_error` | 429 | Rate-limit window exhausted - back off and retry | | `api_error` | 500-599 | Server-side failure - safe to retry | ## HTTP status codes | Status | Typical type | Notes | | ------ | ---------------------------------------------- | --------------------------------------------------------------------------------------- | | `400` | `invalid_request_error` | Validation failure | | `401` | `authentication_error` | Missing or invalid API key | | `402` | `permission_error` | Plan quota or entitlement wall - volume cap, key/endpoint cap, or MCP write entitlement | | `403` | `permission_error` or `account_error` | Permission gate or suspended account | | `404` | `invalid_request_error` | Resource not found or not visible to your project | | `409` | `invalid_request_error` or `idempotency_error` | State conflict, or Idempotency-Key collision | | `410` | `account_error` | Merchant can no longer accept payments | | `422` | `invalid_request_error` | Request is valid but the action cannot be performed | | `423` | `account_error` | MCP token paused for anomalous activity | | `429` | `rate_limit_error` | See `Retry-After`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` headers | | `500` | `api_error` | Server-side failure - safe to retry with backoff | | `503` | `api_error` | Temporary maintenance - retry shortly | ## Error codes ### Authentication | Code | Status | Description | | ------------------------ | ------ | ------------------------------------------------------------------------------------------------------- | | `api_key_missing` | 401 | No `Authorization` header, or empty key portion | | `api_key_invalid` | 401 | Key not recognized - typo, deleted project, or wrong environment | | `api_key_revoked` | 401 | Key was explicitly revoked from the dashboard | | `api_key_expired` | 401 | Key passed its expiration timestamp | | `api_key_must_be_secret` | 403 | Endpoint requires `sk_...`; a publishable `pk_...` was sent | | `account_suspended` | 403 | Merchant account suspended - see `error.suspension` | | `token_paused` | 423 | MCP token paused due to anomalous activity - see `error.pause` | | `forbidden` | 403 | Key doesn't have permission for this action, or the feature requires a higher plan - see `requiredPlan` | ### Resources | Code | Status | Description | | ---------------------- | ------ | ----------------------------------------------------------------- | | `not_found` | 404 | Resource not found or not visible to your project | | `invalid_state` | 409 | Resource is in a state that doesn't allow this action | | `idempotency_conflict` | 409 | Concurrent request with same `Idempotency-Key` is still in flight | ### Billing | Code | Status | Description | | ------------------ | ------ | ---------------------------------------------------------------------------------------------- | | `trial_not_active` | 409 | The subscription's trial is not active (already ended or never started) and cannot be modified | Other billing rule violations return a descriptive `error.message` with `error.type` set to `invalid_request_error` and the HTTP status shown, without a dedicated `error.code` today. This covers a missing plan, coupon, promotion code, or customer (`404`), an inactive coupon or expired promotion code, and a subscription that is not in an active state (`409` or `422`). Branch on `error.type` and the HTTP status, and read `error.message`. Dedicated billing codes are being added incrementally. ### Plans, quotas, and availability These are the gates paying merchants hit as they grow. Each carries extra fields for programmatic handling. | Code | Status | Description | | ---------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------- | | `forbidden` | 403 | A feature requires a higher plan - `requiredPlan`, `currentPlan`, and `entitlement` accompany the error | | `quota_exceeded` | 402 | A plan limit was reached - live monthly volume cap, or an API-key / webhook-endpoint cap. Carries `requiredPlan`, `limit`, and `current` | | `entitlement_required` | 402 | An MCP token's write scopes aren't available on the merchant's current plan - carries `entitlement` | | `insufficient_scope` | 403 | The API key is missing a required scope - the needed scope is in the `WWW-Authenticate` header | | `over_spend_cap` | 403 | An MCP token exceeded its 24h spend cap - carries `spent_paisa`, `cap_paisa`, `remaining_paisa`, and `reset_at` | | `merchant_suspended` | 410 | The merchant can no longer accept payments (hosted checkout / payment-link surfaces) | | `maintenance_mode` | 503 | PayBridgeNP is briefly in maintenance - retry shortly | ### Validation + rate limiting | Code | Status | Description | | ----------------------- | ------ | ------------------------------------------------------------------------ | | `invalid_request_error` | 400 | Generic validation failure - read `error.message` for the specific issue | | `rate_limited` | 429 | Public-API rate-limit window exhausted | ## The `account_suspended` error When your account is suspended, **every API request** returns `403 account_suspended` - including read-only endpoints. Hosted checkout pages and payment links also stop working. ```json theme={null} { "error": { "message": "This merchant account has been suspended. Contact support@paybridgenp.com to resolve.", "type": "account_error", "code": "account_suspended", "request_id": "req_...", "suspension": { "suspended_at": "2026-04-20T10:00:00.000Z", "reason": "Violation of terms of service" } } } ``` Contact [support@paybridgenp.com](mailto:support@paybridgenp.com) to appeal or resolve the suspension. ## Handling errors in code ```typescript TypeScript theme={null} import { PayBridgeNP, AuthenticationError, RateLimitError, InvalidRequestError } from "@paybridge-np/sdk"; const paybridgenp = new PayBridgeNP({ apiKey: process.env.PAYBRIDGENP_API_KEY }); try { const payment = await paybridgenp.payments.retrieve("pay_abc123"); } catch (err) { if (err instanceof AuthenticationError) { // Re-prompt for credentials } else if (err instanceof RateLimitError) { // Back off and retry after err.retryAfter seconds } else if (err instanceof InvalidRequestError) { // Bad input - log err.code and err.message } else { throw err; } } ``` ```python Python theme={null} import paybridge_np from paybridge_np.errors import AuthenticationError, RateLimitError, InvalidRequestError paybridgenp = paybridge_np.PayBridgeNP(api_key=os.environ["PAYBRIDGENP_API_KEY"]) try: payment = paybridgenp.payments.retrieve("pay_abc123") except AuthenticationError: # Re-prompt for credentials pass except RateLimitError as e: # Back off and retry pass except InvalidRequestError as e: # Bad input - log e.code and e.message pass ``` ```php PHP theme={null} use PayBridgeNP\PayBridgeNP; use PayBridgeNP\Exceptions\AuthenticationException; use PayBridgeNP\Exceptions\RateLimitException; use PayBridgeNP\Exceptions\InvalidRequestException; $paybridgenp = new PayBridgeNP(['apiKey' => $_ENV['PAYBRIDGENP_API_KEY']]); try { $payment = $paybridgenp->payments->get('pay_abc123'); } catch (AuthenticationException $e) { // Re-prompt for credentials } catch (RateLimitException $e) { // Back off and retry } catch (InvalidRequestException $e) { // Bad input } ``` ## Migrating from the flat envelope If you have integration code written before the new envelope, here is the upgrade path. The change is breaking - upgrade your SDK and any direct error parsing in lockstep. ### Old shape ```json theme={null} { "error": "Invalid API key", "code": "unauthorized" } ``` ### New shape ```json theme={null} { "error": { "message": "Invalid API key", "type": "authentication_error", "code": "api_key_invalid", "request_id": "req_..." } } ``` ### What changed * `error` is now an object instead of a string. Reading `response.error` directly will give you `[object Object]` instead of the message. * Read `response.error.message` for the human-readable string, `response.error.type` for the broad category, `response.error.code` for the specific identifier. * `code: "unauthorized"` was generic - it has been split into `api_key_missing`, `api_key_invalid`, `api_key_revoked`, `api_key_expired` so you can branch by failure mode. * Suspension fields previously at the top level (`suspended_at`, `suspended_reason`) are now nested under `error.suspension`. * All errors now include `error.request_id` matching the `X-Request-Id` response header. Quote it in support requests. ### SDK versions The TypeScript, PHP, and Python SDKs handle this transparently when you upgrade: | Language | Pre-envelope | Post-envelope | | -------------------------------- | ------------ | ------------- | | TypeScript (`@paybridge-np/sdk`) | `< 3.0.0` | `>= 3.0.0` | | PHP (`paybridge-np/sdk`) | `< 3.0.0` | `>= 3.0.0` | | Python (`paybridge-np`) | `< 1.0.0` | `>= 1.0.0` | # API overview Source: https://docs.paybridgenp.com/api-reference/overview Learn the PayBridgeNP API conventions for authentication, amounts, pagination, retries, and versioning. The PayBridgeNP API uses JSON over HTTPS. All merchant API requests start with this base URL: ```text theme={null} https://api.paybridgenp.com/v1 ``` Send a secret API key as a Bearer token. Never expose an `sk_` key in browser or mobile code. Add an `Idempotency-Key` to create and update requests that might be retried. Branch on the stable error `type` and `code`, and retain the request ID for support. See current PayBridgeNP and provider availability before investigating an integration failure. ## Request conventions | Convention | What to send | | -------------- | -------------------------------------------------------- | | Authentication | `Authorization: Bearer sk_test_...` or `sk_live_...` | | Content type | `Content-Type: application/json` for JSON request bodies | | Amounts | Integer paisa. `10000` means NPR 100.00 | | Currency | `NPR` unless an endpoint says otherwise | | Dates | ISO 8601 timestamps in UTC | | Idempotency | A stable `Idempotency-Key` for a single logical write | A test key does not make every provider payment fake. eSewa and Khalti use test environments, but Fonepay has no sandbox. Fonepay payments from a sandbox project use your real Fonepay account and move real money within the documented caps. ## Pagination List endpoints use offset pagination: ```bash theme={null} curl "https://api.paybridgenp.com/v1/payments?limit=50&offset=0" \ -H "Authorization: Bearer sk_test_your_key" ``` Paginated responses include a `data` array and a `meta` object with `total`, `limit`, and `offset`. Increase `offset` by `limit` until you have read the required range. Do not assume that a short page is permanent when new records can be created during pagination. ## Rate limits and retries Rate-limited API responses return `429`. Read `Retry-After` before retrying. Public API responses may also include: * `X-RateLimit-Limit` * `X-RateLimit-Remaining` * `X-RateLimit-Reset` Retry temporary `429` and `5xx` responses with bounded exponential backoff. Use an idempotency key so retrying a write does not create a second resource. ## Request IDs Every response includes `X-Request-Id`. Error responses repeat the same value as `error.request_id`. Record it with your application logs and include it when contacting support. ## Versioning The current API is namespaced under `/v1`. Additive response fields and new enum values may appear without changing the URL, so ignore fields you do not use and handle unknown enum values safely. Breaking request or response changes require a new version or a documented deprecation period. ## Start building Create a hosted payment session and redirect the customer to its checkout URL. Verify a signed webhook and match status, amount, and currency before fulfillment. # Cancel a payment link Source: https://docs.paybridgenp.com/api-reference/payment-links/cancel-payment-link POST /v1/payment-links/{id}/cancel Deactivates a payment link so it can no longer accept payments, while keeping it (and its history) for your records. Idempotent-safe: returns `422` if the link is already inactive. Requires the `links:write` scope. This is the recommended way to retire a link that has already been used. # Create a payment link Source: https://docs.paybridgenp.com/api-reference/payment-links/create-payment-link POST /v1/payment-links Creates a reusable hosted payment page. Requires the `links:write` scope. Provide either a fixed `amount`, or `minAmount`/`maxAmount` bounds for a customer-entered amount. # Delete a payment link Source: https://docs.paybridgenp.com/api-reference/payment-links/delete-payment-link DELETE /v1/payment-links/{id} Permanently deletes a payment link. Only allowed when the link has never been used. Otherwise it returns `422`, and you should cancel it instead (which keeps it for your records). Requires the `links:write` scope. # Get a payment link Source: https://docs.paybridgenp.com/api-reference/payment-links/get-payment-link GET /v1/payment-links/{id} Retrieves a single payment link by ID, including aggregated view and conversion stats. Requires the `links:read` scope. # List payment links Source: https://docs.paybridgenp.com/api-reference/payment-links/list-payment-links GET /v1/payment-links Returns payment links for the authenticated project, newest first. Requires the `links:read` scope. # Update a payment link Source: https://docs.paybridgenp.com/api-reference/payment-links/update-payment-link PATCH /v1/payment-links/{id} Updates a payment link's editable fields. Only the keys you send are changed. Requires the `links:write` scope. To deactivate a link, set `active` to `false` (or use the cancel endpoint). # Get a payment Source: https://docs.paybridgenp.com/api-reference/payments/get-payment GET /v1/payments/{id} Retrieves a single payment by its ID. # List payments Source: https://docs.paybridgenp.com/api-reference/payments/list-payments GET /v1/payments Returns a paginated list of payments for the project, ordered by most recent first. # List configured providers Source: https://docs.paybridgenp.com/api-reference/providers/list-providers GET /v1/providers Returns the payment providers enabled and configured for the authenticated project. Useful for deciding which "Pay with X" options to render in a custom UI. In sandbox, eSewa and Khalti are always available (platform-managed test credentials) unless explicitly disabled. # Create a Fonepay Direct-QR session Source: https://docs.paybridgenp.com/api-reference/qr/create-fonepay-qr POST /v1/qr/fonepay Mints a Fonepay Dynamic QR server-side, skipping the hosted checkout page. Returns the QR string, a base64 PNG, and an SSE URL to listen for live status. Requires the `payments:write` scope. In live mode, Direct-QR requires Pro or higher. In sandbox, it is available on every plan, but Fonepay sandbox payments can move real money and remain capped. A `403 forbidden` with `requiredPlan` is returned when the mode-aware entitlement check fails. Fonepay must be configured for the project (in the request's mode). Subscribe to `events_url` to receive `qr.scanned` / `qr.paid` / `qr.expired` events. # Refresh a Fonepay Direct-QR (same session) Source: https://docs.paybridgenp.com/api-reference/qr/refresh-fonepay-qr POST /v1/qr/{id}/refresh Regenerate a fresh Fonepay QR for an **existing** Direct-QR session. The Fonepay QR display window is only ~3 min and some wallets (eSewa) reject a stale QR, so call this to show a fresh, scannable QR **without spawning a new session per order** - the session id, `events_url`, and webhook are unchanged. The session's `expires_at` (the 30-min lifetime) is **immutable** and is never extended by a refresh. A payment made on a previously-shown QR is still captured. Requires the `payments:write` scope. **Pro only.** # Create a refund Source: https://docs.paybridgenp.com/api-reference/refunds/create-refund POST /v1/refunds Initiates a refund for a successful payment. **Provider behaviour:** - **Khalti** - fully automated. The refund is processed immediately via the Khalti API, and the response status will be `succeeded` on success or `failed` if Khalti rejects it. - **eSewa** - does not expose a public refund API. The refund is recorded with status `requires_action`. You must process it manually through the [eSewa merchant portal](https://merchant.esewa.com.np) and the record will serve as your audit trail. - **Fonepay** - not available through this API. Fonepay refunds are full-amount only and are issued from the PayBridgeNP dashboard instead; a request against a Fonepay payment returns `422`. **Partial refunds:** For Khalti and eSewa, you can issue multiple partial refunds against a single payment until the total reaches the original payment amount. The `amount` you request may not exceed the payment's remaining unrefunded balance. # Get a refund Source: https://docs.paybridgenp.com/api-reference/refunds/get-refund GET /v1/refunds/{id} Retrieves a single refund by ID. # List refunds Source: https://docs.paybridgenp.com/api-reference/refunds/list-refunds GET /v1/refunds Returns refunds for the authenticated project, newest first. Optionally filter by payment ID. # Send a pending-payment SMS reminder Source: https://docs.paybridgenp.com/api-reference/sms/notify-pending-payment POST /v1/sms/notify-pending-payment Sends a transactional "complete your payment" SMS to a customer, on the merchant's behalf, with the merchant's plan limits, audit trail, and anti-abuse guards applied. Requires the `payments:write` scope. **Guards (all enforced):** - The recipient must have a **pending checkout session on this project within the last 24h** - otherwise `400` (`phone_not_on_session`). - `checkoutUrl` must be on an **allowed PayBridgeNP checkout host** for the project (canonical host or verified custom domain) - otherwise `400` (`invalid_checkout_host`). - Per-merchant abuse limits - otherwise `429` (`rate_limited`). The merchant's SMS plan entitlement is checked inside the dispatcher; `status` reflects the dispatcher outcome. # Delete a webhook endpoint Source: https://docs.paybridgenp.com/api-reference/webhooks/delete-endpoint DELETE /v1/webhooks/{id} Permanently deletes a webhook endpoint. Future events will no longer be delivered to it. # List delivery attempts Source: https://docs.paybridgenp.com/api-reference/webhooks/list-deliveries GET /v1/webhooks/{id}/deliveries Returns the last 50 delivery attempts for a webhook endpoint. Useful for debugging failed deliveries. # List webhook endpoints Source: https://docs.paybridgenp.com/api-reference/webhooks/list-endpoints GET /v1/webhooks Returns all webhook endpoints registered for this project. # Register a webhook endpoint Source: https://docs.paybridgenp.com/api-reference/webhooks/register-endpoint POST /v1/webhooks Registers an HTTPS URL to receive PayBridgeNP event notifications. PayBridgeNP signs every delivery with an HMAC-SHA256 signature using the `signing_secret` returned here. **Save this secret immediately - it is only shown once.** Each delivery carries two headers: `X-PayBridgeNP-Signature: t=,v1=` and `X-PayBridgeNP-Timestamp`. To verify, parse the `t` and `v1` values out of the signature header, compute `HMAC-SHA256(signing_secret, ".")`, and compare the hex digest to `v1` in constant time. Reject deliveries whose timestamp is more than ~300 seconds old to guard against replay. The SDKs' `constructEvent()` helpers do all of this for you; see the [webhook verification guide](https://docs.paybridgenp.com/guides/webhook-verification). # Update a webhook endpoint Source: https://docs.paybridgenp.com/api-reference/webhooks/update-endpoint PATCH /v1/webhooks/{id} Updates the URL, subscribed events, or enabled state of an existing webhook endpoint. Only the fields you include in the request body are changed - omitted fields keep their current value. # Analytics API Source: https://docs.paybridgenp.com/guides/analytics Pull headline payment KPIs, a provider breakdown, and the checkout funnel for a project over a rolling window, in a single call. The Analytics API returns a compact set of key metrics for a project so an AI agent, a programmatic dashboard, or an SDK consumer can read performance without scraping the dashboard. It powers the analytics tooling in the [PayBridgeNP MCP server](/integrations/mcp). This is a read-only convenience endpoint. For row-level data, list [payments](/api-reference/payments/list-payments) instead. For period-over-period growth, query two windows and compare them yourself. ## Scope and mode Authenticate with a secret API key that holds the `analytics:read` scope. Results are scoped to the mode of the calling key. A sandbox key sees sandbox data, a live key sees live data. ## Endpoint ### `GET /v1/analytics/overview` **Query parameters** | Parameter | Type | Default | Description | | --------- | ------- | ------- | --------------------------------------------------------------------------------------------- | | `days` | integer | `30` | Size of the rolling window in days, counted back from today (UTC). Clamped to the range 1–90. | **Request** ```bash theme={null} curl "https://api.paybridgenp.com/v1/analytics/overview?days=30" \ -H "Authorization: Bearer sk_live_your_key" ``` **Response** ```json theme={null} { "window": { "days": 30, "start": "2026-05-16T00:00:00.000Z", "end": "2026-06-15T09:12:44.000Z" }, "payments": { "total": 412, "success": 389, "failed": 23, "success_rate": 0.9441, "success_volume_paisa": 5821000 }, "funnel": { "sessions_created": 540, "sessions_initiated": 470, "sessions_paid": 389, "conversion_rate_initiated_to_paid": 0.8277 }, "by_provider": [ { "provider": "esewa", "count": 250, "success_count": 240, "success_volume_paisa": 3500000 }, { "provider": "khalti", "count": 120, "success_count": 112, "success_volume_paisa": 1600000 }, { "provider": "fonepay", "count": 42, "success_count": 37, "success_volume_paisa": 721000 } ] } ``` **Fields** | Field | Description | | ------------------------------------------ | ------------------------------------------------------------------- | | `window.days` | The effective window size after clamping. | | `window.start` / `window.end` | UTC bounds of the window. `start` is midnight `days` ago. | | `payments.total` | Payment records created in the window. | | `payments.success` / `payments.failed` | Counts by terminal status. | | `payments.success_rate` | `success / total` as a fraction (0 when there are no payments). | | `payments.success_volume_paisa` | Sum of succeeded payment amounts, in paisa. | | `funnel.sessions_created` | Checkout sessions created in the window. | | `funnel.sessions_initiated` | Sessions that reached a provider (initiated, succeeded, or failed). | | `funnel.sessions_paid` | Sessions that resulted in a successful payment. | | `funnel.conversion_rate_initiated_to_paid` | `sessions_paid / sessions_initiated` as a fraction. | | `by_provider[]` | Per-provider count, success count, and succeeded volume in paisa. | All money values are integers in **paisa** (1 rupee = 100 paisa), matching the rest of the API. # Billing Source: https://docs.paybridgenp.com/guides/billing Set up recurring subscriptions and automated invoicing for your customers. The PayBridgeNP Billing system handles recurring payments - create a plan, add customers, and start subscriptions. PayBridgeNP generates invoices automatically at each renewal, sends payment links to customers, and manages retries when payments fail. In **live mode**, Billing requires a **Growth plan or higher**. In **sandbox mode**, every plan can build and test the full Billing lifecycle. [See the sandbox limits](#sandbox-limits). ## Core concepts **Plans** define the recurring charge - amount, interval (monthly, yearly, etc.), trial period, and dunning behavior. Plans are templates; subscriptions are instances. **Customers** are the people being billed. They exist independently of subscriptions - you can have a customer with no active subscriptions. **Subscriptions** link a customer to a plan. When you create a subscription, PayBridgeNP immediately generates the first invoice (unless there's a trial period). **Invoices** are generated automatically at each billing cycle. Each invoice has a payment link that PayBridgeNP sends to the customer. Invoices go from `open` → `paid` once the customer pays, or `overdue` if they don't pay in time. ## Sandbox limits Sandbox Billing is open on every plan with these account-level guardrails: | Resource | Sandbox limit | | -------------------------- | ------------------------------------------------------------------ | | Customers | 25 | | Active plans | 10 | | Non-terminal subscriptions | 25 | | Active coupons | 25 | | Active promotion codes | 25 | | Active dunning policies | 10 | | Delivered invoice emails | 50 per calendar month, sent only to your PayBridgeNP account email | Other sandbox emails are recorded but not sent. A sandbox subscription auto-expires after 12 generated invoices or 30 days without a payment. Coupons and dunning policies are mode-specific, so a sandbox coupon or policy never affects live Billing; the same code can exist once in each mode. For usage, invoice items, subscription discounts, dunning actions, and proration, use a key belonging to the subscription or invoice's project and mode. Another project's key cannot access these records, even within the same account. A proration target plan must also belong to that project and mode. Dunning policy definitions remain shared within an account and mode. Instead of the hosted payment link, you can collect an invoice with a **Fonepay Direct-QR** (`POST /v1/billing/invoices/{id}/qr`) - the customer scans a QR in your own app or at a counter, and on payment the invoice is marked paid and the subscription activates automatically. See [Direct-QR for Fonepay](/guides/direct-qr#pay-a-subscription-invoice-with-direct-qr). *** ## Step 1: Create a plan ```typescript theme={null} const plan = await paybridgenp.plans.create({ name: "Pro Monthly", amount: 99900, // Rs. 999.00 in paisa intervalUnit: "month", intervalCount: 1, // every 1 month trialDays: 14, // 14-day free trial gracePeriodDays: 3, // 3 days after due before going overdue overdueAction: "mark_past_due", }); console.log(plan.id); // bp_... ``` ### Interval options | `intervalUnit` | `intervalCount` | Billing frequency | | -------------- | --------------- | ----------------- | | `month` | `1` | Monthly | | `month` | `3` | Quarterly | | `year` | `1` | Yearly | | `week` | `2` | Every 2 weeks | | `day` | `30` | Every 30 days | ### Dunning settings Dunning controls what happens when a customer doesn't pay on time: | Setting | Description | | ----------------------------- | ----------------------------------------------------------------------------- | | `gracePeriodDays` | Days after due before triggering the overdue action | | `reminderDaysBeforeDue` | Send a reminder email N days before the invoice is due | | `overdueReminderIntervalDays` | Repeat overdue reminders every N days | | `overdueAction` | What to do when overdue: `keep_active`, `mark_past_due`, `pause`, or `cancel` | *** ## Step 2: Create a customer ```typescript theme={null} const customer = await paybridgenp.customers.create({ name: "Aarav Sharma", email: "aarav@example.com", phone: "9801234567", // optional externalCustomerId: "user_123", // optional - your internal user ID }); console.log(customer.id); // bc_... ``` The `externalCustomerId` field lets you link PayBridgeNP customers to your own user records. *** ## Step 3: Create a subscription ```typescript theme={null} const subscription = await paybridgenp.subscriptions.create({ customerId: customer.id, planId: plan.id, }); console.log(subscription.id); // sub_... console.log(subscription.status); // "trialing" (if plan has trialDays > 0) ``` When a subscription is created: * A future `startDate` displays as `draft`. At the start date it displays as `trialing` when a trial follows, otherwise `active`. * A trial displays as `trialing`, and no invoice is generated until the trial ends. * An immediate subscription without a trial starts as `incomplete`. PayBridgeNP generates the first invoice and sends its payment link; the subscription becomes `active` when that invoice is paid. **Do not grant paid access from `status` alone.** `active` means billing is enabled, but it can coexist with an open first invoice after a trial or an overdue renewal when the plan uses `keep_active`. Base access for a billing period on that period's `invoice.paid` event or paid invoice record. For immediate prepaid activation, the subscription stays `incomplete` until the first invoice is paid; if it remains unpaid for 24 hours, it becomes `incomplete_expired` and the invoice is voided. *** ## Subscription lifecycle
Subscription activation routes and the keep active, mark past due, pause, and cancel overdue actions
| Display status | Meaning | | -------------------------------- | ------------------------------------------------------------------------------ | | `incomplete` | Created, first invoice not yet paid - no access until paid | | incomplete\_expired | First invoice was never paid within 24 hours - terminal | | `draft` | Start date is in the future | | `trialing` | Active, in trial period - no invoices yet | | `active` | Billing enabled. Check the invoice for the period before granting paid access. | | `past_due` | A renewal invoice is unpaid past the grace period | | `paused` | Billing paused - no invoices generated | | `cancelled` | Ended - no further invoices | | `completed` | Terminal - no further invoices. Open-ended plans normally end as `cancelled`. | *** ## Managing subscriptions ### Pause ```typescript theme={null} await paybridgenp.subscriptions.pause(subscription.id, { reason: "Customer requested temporary pause", }); ``` No invoices are generated while paused. Resume resumes billing from the current period. ### Resume ```typescript theme={null} await paybridgenp.subscriptions.resume(subscription.id); ``` ### Cancel ```typescript theme={null} // Cancel immediately await paybridgenp.subscriptions.cancel(subscription.id, { reason: "Customer churned", }); // Cancel at end of current period await paybridgenp.subscriptions.cancel(subscription.id, { atPeriodEnd: true, }); ``` ### Change plan ```typescript theme={null} // Change takes effect at the next renewal await paybridgenp.subscriptions.changePlan(subscription.id, { planId: newPlan.id, }); ``` The current period continues on the old plan. At renewal, the subscription switches to the new plan. *** ## Invoices Invoices are read-only from the API - PayBridgeNP creates them automatically. Use them to display billing history to customers. ```typescript theme={null} const { data: invoices } = await paybridgenp.invoices.list({ subscriptionId: subscription.id, }); for (const invoice of invoices) { console.log(invoice.status, invoice.amount_due, invoice.due_at); } ``` | Invoice status | Meaning | | --------------- | ------------------------------------------------ | | `draft` | Not yet sent | | `open` | Sent to customer, awaiting payment | | `paid` | Customer paid | | `overdue` | Not paid by due date | | `void` | Cancelled | | `uncollectible` | Dunning exhausted - written off as unrecoverable | *** ## Webhook events PayBridgeNP fires webhook events throughout the subscription lifecycle: | Event | When | | -------------------------- | ----------------------------------------------------------------------------------------- | | `subscription.created` | New subscription created (may be `incomplete` until first payment) | | `subscription.activated` | First invoice paid, a `past_due` subscription recovered, or a paused subscription resumed | | `subscription.trial_ended` | Trial ended and the first post-trial invoice was generated | | `subscription.expired` | First invoice never paid within 24h - subscription is `incomplete_expired` | | `subscription.past_due` | A renewal invoice went past due | | `subscription.paused` | Subscription paused | | `subscription.cancelled` | Subscription cancelled | | `invoice.created` | New invoice generated | | `invoice.paid` | Customer paid an invoice | | `invoice.overdue` | Invoice went past due | # Confirming a payment Source: https://docs.paybridgenp.com/guides/confirming-payments Know for certain when a payment succeeded. Use webhooks as your source of truth, and verify server-side before you release value. A customer landing on your success page is not proof of payment. Confirm every payment on your server before you release goods, credit, or access. Navigation only. Treat every query parameter as untrusted. Your real-time signal. Verify the raw body before reading the event. Your fallback check. Match status, amount, and currency to your order. ## Webhooks are the source of truth PayBridgeNP sends a signed webhook the instant a payment reaches a terminal state. This is the reliable, real-time signal to act on. Do not build your fulfillment around the browser redirect. Add an HTTPS URL in the [Dashboard](https://dashboard.paybridgenp.com) under **Settings → Webhooks**, or with the [API](/api-reference/webhooks/register-endpoint). Save the signing secret shown once at creation. Every delivery carries an `X-PayBridgeNP-Signature: t=,v1=` header. Parse out `t` and `v1`, compute `HMAC-SHA256(signing_secret, ".")`, and compare the hex digest to `v1` in constant time before you trust the payload. Or let the SDK's `constructEvent()` do it. Full walkthrough in [Verifying webhooks](/guides/webhook-verification). Fulfill on `payment.succeeded`, and release any hold on `payment.failed`. Deliveries are idempotent and can be retried, so key your fulfillment on the payment `id` and ignore a repeat you have already handled. Never fulfill an order from return-URL parameters or client-side JavaScript alone. A customer can change them. Fulfill from a signature-verified webhook on your server. ## Payment states
Payment states from pending and processing to success, failed, cancelled, or refunded
## When a payment fails or the buyer cancels Hosted checkout does not leave the buyer on a dead end. If a payment fails, or the buyer cancels partway through, returning to the same checkout link while the session is still valid brings back the same session at the method picker, with a notice naming the provider that failed and inviting the buyer to try again or pick a different method. The provider that just failed is de-prioritized in the picker rather than pre-selected, so the retry nudges the buyer toward a different route. Because the same session is reused rather than a new one being created, a recovered checkout cannot double-charge: only one attempt on that session can end in a successful payment. The retry does not buy extra time either. The session's 30-minute expiry window starts at creation, and a failed or cancelled attempt does not extend it. This matters for your webhook handling. A session that simply times out unpaid fires no webhook at all. A buyer-initiated cancellation does fire `payment.cancelled`, and because a buyer can cancel more than one attempt on the same session before succeeding or giving up, you may receive `payment.cancelled` more than once for a single session. Treat repeats as informational rather than as a reason to change anything you have already recorded for that session. ### Amount mismatch If the provider reports a paid amount that differs from what the session billed, PayBridgeNP does not record a success. The payment is stored as failed with the decline reason **Amount mismatch**, the order is never marked paid, no receipt goes out, and `payment.failed` is sent with `reason` set to `Amount mismatch: expected , received `. In live mode, the amount the buyer actually paid has still settled into your provider account, because the provider moves the money before PayBridgeNP sees the result. Reconcile it against your bank or wallet statement, and if you want to return it to the buyer, refund it from the provider's merchant panel. The payment's page in the dashboard shows the billed and reported amounts side by side. In test mode, the eSewa and Khalti sandboxes move no real money, so a test-mode mismatch has nothing to refund. Fonepay has no sandbox: a test-mode Fonepay payment runs on your real merchant account and is real money (see [Sandbox testing](/guides/sandbox-testing)). ## Verify before you release value When a webhook is delayed, or you want a second confirmation before handing over something valuable, read the payment back and check its status yourself. The payment object reports `status: "success"` once it is paid. ```bash cURL theme={null} curl https://api.paybridgenp.com/v1/payments/pay_01j9x2k3m4n5p6q7r8s9t0u1v2 \ -H "Authorization: Bearer sk_live_your_key" ``` ```javascript Node.js theme={null} const order = await loadOrderFromDatabase("ORD-001"); const payment = await paybridgenp.payments.retrieve("pay_01j9x2k3m4n5p6q7r8s9t0u1v2"); if ( payment.status === "success" && payment.amount === order.amountPaisa && payment.currency === order.currency ) { await fulfillOrderOnce(order.id, payment.id); } ``` ```python Python theme={null} order = load_order_from_database("ORD-001") payment = client.payments.retrieve("pay_01j9x2k3m4n5p6q7r8s9t0u1v2") if ( payment["status"] == "success" and payment["amount"] == order.amount_paisa and payment["currency"] == order.currency ): fulfill_order_once(order.id, payment["id"]) ``` Before you release anything, confirm three things: the `status` is `success`, and the `amount` and `currency` match what you charged. ## Do not poll in a tight loop Reading the payment endpoint on a fast timer is unreliable and will run into rate limits. Let webhooks do the waiting. Use [`GET /v1/payments/{id}`](/api-reference/payments/get-payment) for a one-off check or to reconcile a specific payment after the fact, not as a polling loop. ## Reconciliation To reconcile a batch against your own records, page through your payments with `limit` and `offset` and match each one: ```bash theme={null} curl "https://api.paybridgenp.com/v1/payments?limit=100&offset=0" \ -H "Authorization: Bearer sk_live_your_key" ``` Every payment carries a stable `id`, `status`, `amount`, `provider`, and `provider_ref` you can match against your ledger. Page with `offset` until you have covered the window you care about. # Coupons and promotion codes Source: https://docs.paybridgenp.com/guides/coupons Discount subscriptions with coupons (reusable templates) and promotion codes (customer-facing handles). PayBridgeNP uses a two-level discount model: a **coupon** is a reusable discount template; a **promotion code** is a customer-facing string (like `LAUNCH50`) that redeems a coupon. ## Concepts ### Coupon The template. Defines: * **Discount**: `percent` (1-100%) or `amount` (fixed paisa off) * **Duration**: `once` (next invoice only), `repeating` (N cycles), or `forever` * **Caps**: `maxRedemptions`, `redeemBy` expiry * **Scope**: `appliesToPlanIds` (nullable = all plans) ### Promotion code A public handle pointing at a coupon. Has its own independent limits: * `maxRedemptions` * `expiresAt` * `minimumAmount` (order floor in paisa) * `customerIds` restriction A single coupon can have multiple promotion codes - useful for split tests (`LAUNCH50` vs `WELCOME50`). ## Creating a coupon ```bash theme={null} curl -X POST https://api.paybridgenp.com/v1/billing/coupons \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "code": "LAUNCH", "name": "Launch discount", "discountType": "percent", "percentOff": 50, "duration": "repeating", "durationInCycles": 3, "maxRedemptions": 100 }' ``` ## Creating a promotion code ```bash theme={null} curl -X POST https://api.paybridgenp.com/v1/billing/promotion-codes \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "couponId": "cpn_…", "code": "LAUNCH50", "maxRedemptions": 500, "expiresAt": "2026-06-01T00:00:00Z" }' ``` The code is case-insensitive and stored uppercase. ## Validating before applying ```bash theme={null} curl -X POST https://api.paybridgenp.com/v1/billing/promotion-codes/validate \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{"code":"LAUNCH50","planId":"bp_…","amount":50000}' ``` Returns `{ valid: true, coupon, promotion_code, discount_preview }` or `{ valid: false, reason }`. This is read-only and safe to call on every keystroke. ## Applying to a subscription ```bash theme={null} curl -X POST https://api.paybridgenp.com/v1/billing/subscriptions/sub_…/apply-coupon \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{"promotionCode":"LAUNCH50"}' ``` Or pass `couponId` directly to skip the promo-code layer. ## Duration semantics | Duration | Behavior | | ----------- | ------------------------------------------------------------------- | | `once` | Applied to the **next** invoice only. Deactivated after. | | `repeating` | Applied for the next `durationInCycles` invoices. Auto-deactivates. | | `forever` | Applied to every future invoice until the coupon is deactivated. | ## Redemption atomicity Coupon and promotion code counters update atomically against the redemption cap - racing N parallel applies can never exceed it. The N-1 losers get an error like `"Promotion code redemption limit reached"`. Safe to retry. ## Invoice math When an invoice is generated for a subscription with an active discount, PayBridgeNP emits line items in this order: 1. **Subscription** (plan amount, positive) 2. **Discount** (coupon discount, negative) 3. **Tax** (VAT on the discounted amount - [see Tax guide](/guides/tax-vat)) Totals apply the discount **before** tax: ``` subtotal = subscription line amount discount_total = |discount line amount| taxable_base = max(0, subtotal - discount_total) tax_total = round(taxable_base * rate_bps / 10000) amount_due = subtotal - discount_total + tax_total ``` ## Webhooks | Event | When it fires | | ------------------------------- | ----------------------------------------------- | | `coupon.created` | Coupon created via API or dashboard. | | `coupon.deleted` | Deactivation. | | `coupon.expired` | Daily sweep deactivates expired coupons. | | `promotion_code.redeemed` | Each time a promo is applied to a subscription. | | `subscription.discount_applied` | Coupon attached to a sub. | | `subscription.discount_removed` | Active discount cleared. | ## Disabling the feature Want coupons switched off for your account? Contact [support@paybridgenp.com](mailto:support@paybridgenp.com) and we'll disable them; existing subscriptions then invoice exactly as they did before coupons were introduced. # Custom Domain Source: https://docs.paybridgenp.com/guides/custom-domain Serve hosted checkout on your own subdomain - pay.yourstore.com instead of checkout.paybridgenp.com - for a fully white-labelled customer experience. By default, every PayBridgeNP checkout session opens at `https://checkout.paybridgenp.com/checkout/cs_...`. With a custom domain, you can route those URLs through a subdomain you control - `https://pay.yourstore.com/checkout/cs_...` - so your customers never leave your brand. Custom domains are a Pro feature. Free and Growth merchants stay on `checkout.paybridgenp.com`. ## What changes, what stays **Changes:** the host portion of every checkout URL the API returns - in `POST /v1/checkout` responses, payment-link emails, payment-button auto-config, dashboard previews. Provider redirects after payment land back on your custom domain. **Stays the same:** * The hosted-checkout HTML, themes, PB ID flow, SMS receipts - identical experience to canonical. * Provider callback URLs (eSewa, Khalti, Fonepay return URLs) stay on `checkout.paybridgenp.com`. Each provider only knows one return URL per merchant; we redirect the customer back to your custom domain after the provider hop, so the visible URL switch is brief (about a second). * Your dashboard, API base URL, payment-link short URLs (`/pay/lnk_...`), and embed scripts stay on PayBridgeNP. Custom domains are checkout-only. ## What you can use * Any subdomain you own (`pay.yourstore.com`, `checkout.yourstore.com.np`, `pay.staging.yourstore.com`). * One hostname **per project**. Multi-brand merchants can run multiple PayBridgeNP projects, each with its own domain - `pay.brandA.com` on project A, `pay.brandB.com` on project B. Sessions created with project A's API key emit `pay.brandA.com/checkout/cs_...`; project B's emit `pay.brandB.com/...`. Sibling projects can't borrow each other's domains (a session URL hand-crafted onto the wrong project's host returns 404). ## What you can't use * Apex domains (`yourstore.com` directly). Use a subdomain. * `paybridgenp.com` and any of its subdomains. * An IP address. ## Setting it up Go to **Settings → Custom domain** in your [dashboard](https://dashboard.paybridgenp.com/settings/custom-domain). The page lists all your projects - click **Add domain** on the project you want to attach a domain to. Type your subdomain (`pay.yourstore.com`) and confirm. The dashboard shows two records: | Type | Name | Value | | ------- | ------------------------------------- | ------------------------------ | | `CNAME` | `pay.yourstore.com` | `cname.paybridgenp.com` | | `TXT` | `_paybridge-verify.pay.yourstore.com` | `pbv_xxxxxxxxxxxxxxxxxxxxxxxx` | The CNAME routes checkout traffic to PayBridgeNP. The TXT proves you own the domain. `cname.paybridgenp.com` is a dedicated routing hostname, separate from the public checkout URL. PayBridgeNP looks up the TXT record. On match, the status pill flips to "Issuing certificate" while a TLS certificate is provisioned for your hostname. The status pill flips to "Active" once the cert is live. New checkout sessions immediately return URLs on your custom domain. DNS changes can take a few minutes to propagate after you save them at your DNS provider. If verification fails, give it 2-3 minutes and click **Verify DNS** again. ## DNS provider notes Most DNS providers (Cloudflare, Route 53, Google Cloud DNS, GoDaddy, Namecheap) accept the records exactly as shown. A few quirks worth knowing: * **Cloudflare proxy:** If your DNS is on Cloudflare, set the CNAME to **DNS only** (gray cloud). The orange-cloud proxy will block PayBridgeNP from reaching your hostname. * **TXT record name:** Some providers want just the prefix (`_paybridge-verify.pay`) instead of the full FQDN (`_paybridge-verify.pay.yourstore.com`). Both are correct - use whichever your provider expects. * **CNAME on apex:** Some providers don't allow CNAME records at the apex of a zone. That's fine - we don't support apex domains anyway. Always use a subdomain. ## Lifecycle Your domain moves through a small state machine: | Status | Meaning | | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | **Awaiting DNS** | The TXT record hasn't been verified yet. Click **Verify DNS** after you've added it. | | **Issuing certificate** | TXT verified; a TLS certificate is being provisioned. Usually 1-5 minutes. | | **Active** | Live. New checkout URLs use your custom domain. | | **Error** | Something failed (cert issuance blocked, hostname revoked upstream). The dashboard shows the underlying error. Remove the domain to start over. | The dashboard auto-checks pending domains; you can also click **Refresh status** to force a re-poll. ## Removing a custom domain Click **Remove** in the dashboard. New checkout sessions immediately switch back to `checkout.paybridgenp.com`. Sessions already issued on the custom domain keep working until they expire (about 30 minutes), then the URL stops resolving. ## What about my existing CNAME? Once your domain is active, **leave the CNAME in place forever**. Removing it stops traffic the next time DNS caches clear. The TXT record can be removed after activation (we only check it during verification), but most merchants leave both in place. ## Plan changes Custom domains are bundled with Pro. If you downgrade to Growth or Free: * Your custom domain is removed from PayBridgeNP within seconds (eager revoke). * New checkout URLs go back to `checkout.paybridgenp.com`. * The custom hostname is removed, so `https://pay.yourstore.com` stops resolving shortly after. You can re-add the same hostname after upgrading back to Pro - DNS is preserved, you just verify again. ## FAQ Not in v1. We require a subdomain (`pay.yourstore.com`, `checkout.yourstore.com`). Briefly - about one second during the provider redirect. After the provider (eSewa, Khalti, Fonepay) processes the payment, they redirect to `checkout.paybridgenp.com/v1/callbacks/...`, and our API immediately redirects the customer back to `https://pay.yourstore.com/checkout/cs_xxx/...`. The flash is unavoidable while the same merchant has one provider return URL on file. Yes. Both surfaces honor your custom domain automatically once it's active. Payment-link short URLs (`paybridgenp.com/pay/lnk_...`) stay on PayBridgeNP - those are intentionally short and brand-neutral - but the checkout page they redirect to renders on your custom domain. Custom domains are checkout-only in v1. Your dashboard stays on `dashboard.paybridgenp.com`, the API on `api.paybridgenp.com`. Email "from" addresses are handled separately - see [email branding](/guides/per-project-branding) for those settings. The success page renders on your custom domain too. Same for failed, cancelled, and expired sessions. Provider callbacks always come back to canonical, but the customer-facing render is on your domain throughout. No. Custom domains are included in Pro. Check three things: 1. The TXT record value matches exactly - copy it from the dashboard, no extra quotes. 2. If you're on Cloudflare, the CNAME and TXT records are set to **DNS only** (gray cloud), not proxied. 3. Give it 2-3 minutes. DNS changes can take time to propagate, especially for first-time records. If it still fails, the error toast will tell you exactly what we saw - either no TXT record found, or a TXT record with a different value. Hosted checkout pages, success pages, failed pages, cancelled pages, and expired-session pages render on your custom domain. Your dashboard and API traffic stay on PayBridgeNP domains. ## Related * [Payment Buttons](/guides/payment-buttons) - embeddable checkout widgets that pick up your custom domain automatically. * [Payment Links](/guides/payment-links) - share a one-off pay URL; the destination renders on your custom domain. * [PayBridgeNP ID](/guides/paybridgenp-id) - one-tap autofill for returning buyers, on both canonical and custom-domain checkouts. # Direct-QR API Source: https://docs.paybridgenp.com/guides/direct-qr Generate a Fonepay QR on your own page and listen for the payment in real time, with no redirect to a hosted checkout. Direct-QR puts a Fonepay QR on your own page. One API call returns the QR image and a live browser event stream. Use the stream to update the page and the signed `payment.succeeded` webhook to fulfill the order. In **live mode**, Direct-QR requires a **Pro plan or higher**. In **sandbox mode**, it is available on every plan. Fonepay sandbox QR payments can move real money, so the NPR 1,000 per-payment and NPR 5,000 monthly caps still apply, and every QR minted in the last 30 minutes counts at full value towards the monthly cap until it is paid or the 30 minutes pass, even if it was replaced or never scanned. ## When to use this Use Direct-QR for a custom app, point-of-sale terminal, or product page that must show the QR in place. For most websites, [hosted checkout](/api-reference/checkout/create-session) is simpler and supports eSewa, Khalti, and Fonepay. Direct-QR is Fonepay-only. ## How it works
Direct QR sequence from server creation through live browser events and the signed payment webhook
Your server calls `POST /v1/qr/fonepay` and sends the returned QR image and `events_url` to the page. The page opens an `EventSource`; PayBridgeNP sends `qr.scanned` and `qr.paid` there while independently delivering the signed webhook to your server. ## Endpoint ### `POST /v1/qr/fonepay` Authenticated with a secret API key. **Request** ```json theme={null} { "amount": 10000, "currency": "NPR", "customer": { "name": "Ram Sharma", "email": "ram@example.com", "phone": "9800000000", "address": { "line1": "Thamel-12", "city": "Kathmandu" } }, "metadata": { "order_id": "ORD-123" } } ``` | Field | Required | Notes | | ------------------ | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `amount` | Yes | Integer paisa. Min 1,000 (Rs. 10), max 100,000,000 (Rs. 10,00,000). | | `currency` | No | Defaults to `"NPR"`. Fonepay only supports NPR. | | `customer.name` | Yes | Up to 100 chars. | | `customer.email` | Yes | Valid email. Used for the receipt. | | `customer.phone` | Recommended | Nepali mobile (`98XXXXXXXX`), up to 30 chars. Used for the live SMS receipt when your plan includes SMS. Sandbox SMS is logged but never delivered. | | `customer.address` | No | If present, `line1` and `city` are required. | | `metadata` | No | Arbitrary JSON object echoed back on webhooks. | **Send `customer.phone` when you have it.** In live mode, it is the only way the buyer can receive an SMS payment confirmation. In sandbox, the rendered SMS is recorded in your logs but never sent to a real phone. **Response (`201`)** ```json theme={null} { "id": "cs_vOKk570ctWssNmFPcFiGfCnM", "amount": 10000, "currency": "NPR", "provider": "fonepay", "status": "initiated", "qr_message": "00020101021226580011fonepay.com...", "qr_image": "data:image/png;base64,iVBORw0KGgo...", "events_url": "https://api.paybridgenp.com/v1/qr/cs_vOKk.../events", "expires_at": "2026-04-25T00:25:05.347Z" } ``` | Field | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | Session ID. Doubles as the `events_url` token - anyone with this ID can listen to events for it, so keep it confidential. | | `qr_message` | The raw EMV QR payload. Render it yourself with any QR library if you want full control over size, colors, or to add a logo in the middle. | | `qr_image` | A 320×320 PNG embedded as a data URL. Drop this straight into an `` if you don't want to render your own. | | `events_url` | Open an `EventSource` on this to receive `qr.scanned`, `qr.paid`, `qr.expired`, and `ping` events. | | `expires_at` | The QR's display window (\~3 min, set by us - not Fonepay). After it, `qr.expired` fires so you can show a fresh QR. It is **not** a payment deadline: a late payment can still confirm (see [Expiry and retries](#expiry-and-retries)). | ### `GET /v1/qr/:id/events` A Server-Sent Events stream. **No API key required** - the session ID in the URL is the auth token, so this is safe to call directly from a customer's browser. | Event | When it fires | Payload | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | | `connected` | Right after the stream opens. | `{ id, status }` | | `qr.scanned` | The customer opened the QR in their bank app. | `{ sessionId, scannedAt }` | | `qr.paid` | Payment confirmed by Fonepay. Terminal - the stream closes after this. | `{ paymentId, sessionId, amount, currency, provider, traceId }` | | `qr.expired` | The \~3-min display window lapsed. **Not terminal** - a late payment can still confirm, so `qr.paid` may still follow. Use it to refresh the QR, never to fail the order. | `{ sessionId, expiredAt }` | | `ping` | Keepalive every 7s. Ignore it. | `{}` | **Replay on reconnect**: if the connection drops and the client reconnects after a terminal event already fired, the stream replays it once with `replay: true` set. So you can safely use the browser's auto-reconnect without missing the payment. ### `POST /v1/qr/{id}/refresh` The Fonepay QR's display window is only \~3 minutes, and some wallets (eSewa) reject a stale QR. Call this to mint a **fresh QR for the same session** - same `id`, same `events_url`, same webhook - instead of spawning a brand-new session per order. It requires the `payments:write` scope and the same mode-aware plan access as QR creation. It takes **no body** (the amount and customer already live on the session). The response is the same shape as create, with a new `qr_message`, `qr_image`, and `expires_at`: ```bash theme={null} curl -X POST https://api.paybridgenp.com/v1/qr/cs_01j9x2k3m4n5p6q7r8s9t0u1v2/refresh \ -H "Authorization: Bearer sk_live_your_key" ``` The session's overall 30-minute lifetime is **not** extended - only the \~3-min QR display window resets. Keep your `EventSource` open across refreshes: the session id is unchanged, so `qr.scanned` / `qr.paid` keep flowing on the same stream. A refresh returns `400` if the session is already paid, expired, or otherwise not refreshable. **One live QR per session.** A refresh only mints a new QR when the current one is gone or inside its last 20 seconds. If the current QR is still fresh, you get it back with its existing `expires_at` (same shape, no new QR). So call refresh on `qr.expired` or when `expires_at` is close, not on a fixed timer to "reset the countdown". If minting is briefly unavailable you get `503` with `code: "fonepay_busy"`; retry after a moment. ## Full example ```html theme={null}
``` Server side (Node/Bun example): ```js theme={null} // /api/start-payment import { Hono } from "hono"; const app = new Hono(); app.post("/api/start-payment", async (c) => { const body = await c.req.json(); const r = await fetch("https://api.paybridgenp.com/v1/qr/fonepay", { method: "POST", headers: { "Authorization": `Bearer ${process.env.PAYBRIDGENP_SECRET_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); return c.json(await r.json(), r.status); }); ``` ## Webhooks vs SSE Use SSE for immediate browser feedback. Use the signed, retried `payment.succeeded` [webhook](/guides/webhook-verification) to update your database and fulfill the order. If the customer closes the page, the webhook still arrives. ## Expiry and retries The QR has a **\~3-minute display window** (set by us, not Fonepay). When it lapses, `qr.expired` fires as a **UI hint** so you can show a fresh QR - but it is **not** the end of the payment: * **A late payment still completes.** Some wallets (eSewa) reject a QR once the window passes, but others (Khalti) let the customer pay later. If they do, Fonepay confirms it - even a few minutes late - and both `qr.paid` (SSE) and the `payment.succeeded` webhook still fire. **Never mark the order failed just because `qr.expired` fired** - wait for `qr.paid`, the webhook, or the session's full lifetime. * **Refresh the same session.** When `qr.expired` fires or `expires_at` is close, call `POST /v1/qr/{id}/refresh`. Keep the same `id`, `events_url`, webhook, and open `EventSource`. * **Settle on the webhook.** Treat `payment.succeeded` as the source of truth for fulfilling the order - it fires whenever Fonepay confirms, fast or late. Use SSE only for live UI. ## Pay a subscription invoice with Direct-QR If you use [subscription billing](/guides/billing), you can collect a subscription's invoice with a Direct-QR instead of sending the customer to the hosted bill page - perfect for in-person or embedded recurring collection (a counter membership, monthly tuition, etc.). ```bash theme={null} curl -X POST https://api.paybridgenp.com/v1/billing/invoices/{id}/qr \ -H "Authorization: Bearer sk_live_your_key" ``` `POST /v1/billing/invoices/{id}/qr` mints a Fonepay Direct-QR for that invoice (amount + customer come from the invoice - no body). When the customer scans and pays, the invoice is marked paid and the subscription **activates** (`incomplete`→`active`) automatically - the same outcome as paying via the bill page. The returned session is a normal Direct-QR session, so the `events_url` SSE stream and `POST /v1/qr/{id}/refresh` both work on it. Requires the `billing:write` scope (available on Growth and higher) and Fonepay credentials configured on the project. ## Limitations * Fonepay only. Other providers don't have an equivalent QR-with-realtime-confirmation flow exposed today. * NPR only. * No partial captures or holds - the QR is a one-shot full-amount transfer. * Refund successful Direct-QR payments from the PayBridgeNP dashboard. Fonepay refunds are not available through the create-refund API. ## Related * [Hosted checkout](/api-reference/checkout/create-session) - the standard multi-provider flow with no Pro gate. * [Webhook verification](/guides/webhook-verification) - verify `payment.succeeded` deliveries on your server. * [Sandbox testing](/guides/sandbox-testing) - Fonepay has no test environment; sandbox Fonepay uses your real merchant credentials with small platform caps. # Dunning (overdue invoice retries) Source: https://docs.paybridgenp.com/guides/dunning Automatically chase overdue invoices with configurable retry schedules before taking a final action. ## Overview Dunning is the process of systematically reminding customers to pay overdue invoices. PayBridgeNP's dunning engine lets you define **retry intervals** and a **final action** (cancel, pause, or write off) so your cash collection happens automatically without manual intervention. ## How it works 1. An invoice is issued and becomes overdue (past its `due_at` date without being paid). 2. If a dunning policy is assigned to the subscription (or the merchant has a default policy), the dunning cycle starts. 3. At each interval, a reminder email is sent and a `invoice.dunning_attempt` webhook fires. 4. When all retries are exhausted, the configured `finalAction` is applied and an `invoice.dunning_exhausted` webhook fires. 5. If the customer pays at any point during the cycle, the cycle stops and an `invoice.paid` webhook fires. ## Creating a dunning policy ```typescript theme={null} const policy = await paybridgenp.dunning.createPolicy({ name: "Standard 3-strike", retryIntervalsDays: [1, 3, 7], // remind at +1d, +3d, +7d after overdue finalAction: "cancel", // 'cancel' | 'pause' | 'mark_uncollectible' isDefault: true, // use for all subscriptions unless overridden }); ``` ### Retry intervals `retryIntervalsDays` must be a non-empty array of whole numbers from 1 to 365, measured in days since the invoice went overdue. Creation accepts `name`, `retryIntervalsDays`, `finalAction`, and `isDefault`; updates also accept `active`. The name must be non-empty after trimming. Ownership and mode come from your API key and cannot be changed through the policy body. | Array | Behavior | | ----------- | -------------------------------------------------------------------- | | `[1, 3, 7]` | Remind on day 1, day 3, day 7. After day 7 exhausted → final action. | | `[2, 5]` | Two reminders. Shorter grace period. | | `[1]` | Single reminder, then final action the next day. | ### Final actions | Value | Effect | | -------------------- | ------------------------------------------------------------------------ | | `cancel` | Subscription cancelled immediately when retries exhausted. | | `pause` | Subscription paused. Customer can pay the invoice later to resume. | | `mark_uncollectible` | Invoice marked uncollectible. Subscription is left in its current state. | ## Assigning a policy to a subscription Leave the default policy for most cases. Override per-subscription when needed: ```typescript theme={null} await paybridgenp.dunning.setSubscriptionPolicy(subscriptionId, policyId); // Revert to merchant default: await paybridgenp.dunning.setSubscriptionPolicy(subscriptionId, null); ``` ## Checking dunning status on an invoice ```typescript theme={null} const status = await paybridgenp.dunning.getInvoiceStatus(invoiceId); // { // status: 'retrying', // attempt_count: 2, // next_attempt_at: '2026-04-25T00:00:00Z', // attempts: [...] // } ``` ## Manually retrying or stopping ```typescript theme={null} // Immediately trigger the next retry (useful for testing): await paybridgenp.dunning.retryInvoiceNow(invoiceId); // Stop all further retries for an invoice: await paybridgenp.dunning.stopInvoice(invoiceId); ``` ## Webhooks | Event | Fired when | | --------------------------- | --------------------------------------------------------------------- | | `invoice.dunning_attempt` | A reminder is sent (includes `attempt_number` and `next_attempt_at`). | | `invoice.dunning_exhausted` | All retries used - final action applied. Includes `final_action`. | | `invoice.paid` | Customer pays during the dunning cycle - cycle stops automatically. | ## Co-existence with plan overdueAction Each billing plan has an `overdueAction` setting (`keep_active`, `mark_past_due`, `pause`, `cancel`). Dunning and `overdueAction` are complementary: * If a subscription has a dunning policy, the policy governs collection. * Recommendation: set the plan's `overdueAction` to `mark_past_due` when using dunning, so the subscription remains accessible while reminders are in flight. ## Dashboard Manage dunning policies at **Billing → Dunning** in the dashboard. You can create multiple policies, set a default, and deactivate old ones. # Embedded Checkout Source: https://docs.paybridgenp.com/guides/embedded-checkout Open the checkout in an overlay on your own page instead of redirecting the customer to a payment page. Embedded Checkout opens the hosted payment flow over your page, so the merchant page stays visible and the address bar does not change. It uses the same providers, webhooks, and server-side verification as the [quickstart](/quickstart). The chosen wallet may still open its app or website. Fonepay stays in the overlay, as does eSewa Intent; Khalti hands off and returns. In **live mode**, Embedded Checkout requires a **Growth plan or higher**. In **sandbox mode**, it is available on every plan. API-created sessions and [payment buttons](/guides/payment-buttons) use the same mode-aware access rule. Embedded Checkout is live. Nothing changes for an existing button until you tick **Open over my own page** on it and add your origin to the embed allowlist, so buttons keep redirecting until you opt them in. ## Hosted or Embedded | | Hosted Checkout | Embedded Checkout | | ------------------------- | ---------------------------- | -------------------------- | | Where the customer pays | On a PayBridgeNP page | In an overlay on your page | | Address bar | Changes to the checkout URL | Never changes | | Integration | Redirect to the returned URL | Add the button script | | Payment methods | eSewa, Khalti, Fonepay | eSewa, Khalti, Fonepay | | Verification and webhooks | Identical | Identical | | Settlement | Your own merchant accounts | Your own merchant accounts |
Embedded PayBridgeNP checkout open over a merchant product page
## How it works Configure it as you would a normal [payment button](/guides/payment-buttons), then tick **Open over my own page** under "Where the checkout opens". Buttons redirect to a PayBridgeNP checkout page until you do, so an existing button never changes behaviour on its own. Register your web page origin under **Project -> Embed domains** in your merchant dashboard. The same one-line embed. No separate SDK. The overlay opens over your page. Your page is never navigated. The overlay closes and returns them to your return URL at the top level, not inside the frame. ```html theme={null} ``` ## If your shopper is already signed in Pass what you know and the overlay opens with it filled in: ```html theme={null} ``` This matters more in the overlay than anywhere else. The overlay collects contact details itself, so without this a signed-in shopper would retype details your site already had. See [payment buttons](/guides/payment-buttons) for the full rules. ## Use your own button If our injected button does not fit your design, keep your own element and open the overlay from it. Add `data-render="none"` to the script tag - it renders nothing at all - and call `window.PayBridgeNP.open()` from your own click handler: ```html theme={null} ``` Everything else behaves exactly as if our button had been tapped: the same overlay, the same fallbacks, the same return to your page. Prefill attributes work unchanged. A few rules: * With several buttons on one page, pass the id: `window.PayBridgeNP.open("btn_your_button_id")`. * `open()` returns `true` when the overlay is opening, and `false` with a `console.warn` explaining why when it is not - an unknown id, config still loading, or a checkout already in flight. Loading is worth handling: a shopper can tap your button before our script has fetched the button's config, so either disable your button until `window.PayBridgeNP` exists or retry on a short delay when `open()` returns `false`. * `open()` works on rendered buttons too. `data-render="none"` only controls whether we inject our element, not whether the API exists. ## Cart totals and per-product prices A payment button carries one fixed amount. That suits a single product, a service fee or a donation, and it does not suit a cart: you would need one button per product and still have no way to charge a cart total. For those, your server creates the session and your page frames it. The amount comes from your server, so a shopper cannot change the price in their browser. This is also the path for a product page where each product has its own price. The same allowlist buttons use, under **Project -> Embed domains**. It is per mode, so register sandbox and live separately. Pass the origin of the page that will frame it. We check it against your registered domains as the session is created, so a typo fails right there with a 400 instead of turning into a blank frame later. Hand the returned `checkout_url` to `window.PayBridgeNP.openSession()`. You get the same overlay a button gets: our backdrop, our mobile sizing, the ready handshake, close-and-reopen safety, and the fallback to a full checkout page if the frame cannot mount. Same as a button. They return to your return URL at the top level, not inside the frame, with the session id and status. Your server creates the session: ```bash theme={null} curl -X POST https://api.paybridgenp.com/v1/checkout \ -H "Authorization: Bearer sk_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "amount": 249900, "currency": "NPR", "returnUrl": "https://your-shop.com/order/complete", "embedOrigin": "https://your-shop.com", "customer": { "name": "Sita Sharma", "email": "sita@example.com", "phone": "9812345678" } }' ``` `amount` is in paisa, so `249900` is Rs 2,499. Your page opens it. Load the script with no `data-button-id`, since there is no button here, and it gives you the API and nothing else: ```html theme={null} ``` `openSession()` returns `true` when the overlay is opening. It returns `false` with a `console.warn` when it will not: a checkout is already open on the page, or the value you passed is not a session URL. Pass the `checkout_url` from the API response as-is rather than rebuilding it, since anything that is not a real session URL is refused. A few rules: * `embedOrigin` cannot be combined with `flow: "redirect"`. Pick one. * The stored origin is re-checked against your current allowlist every time the page renders. Remove a domain and sessions already open stop framing, so it is a marker you can revoke rather than a standing grant. * Live mode requires Growth or higher; sandbox is available on every plan. ## Registering allowed embed domains To protect your site and your customers from clickjacking, Embedded Checkout uses an explicit origin allowlist. A page attempting to embed your checkout whose origin is not registered on your allowlist will be blocked by the browser. ### The symptom of an unallowed domain If your site origin is not registered on your project allowlist: * The checkout overlay opens as a blank frame and silently refuses to load. * Your browser developer console displays a Content Security Policy error: `Refused to frame 'https://checkout.paybridgenp.com/' because an ancestor violates the following Content Security Policy directive: "frame-ancestors ..."` To fix this, register your page's exact web origin under **Project -> Embed domains** in your merchant dashboard. ### Domain allowlist rules * **Exact origins only**: Enter full origins such as `https://shop.example.com` or `http://localhost:3000`. Do not include paths (`/checkout`), query parameters (`?ref=1`), fragment identifiers, or trailing slashes. Wildcards (like `*.example.com`) are not permitted. * **Apex and WWW are distinct**: `https://example.com` and `https://www.example.com` are two separate web origins. If your customers access your shop on both forms, register both origins in your allowlist. * **Scheme and environment rules**: Live mode requires secure `https://` origins. Unencrypted `http://` schemes and private or loopback IP addresses are rejected in Live mode. Sandbox mode permits `https://` as well as `http://` for local testing on `localhost` and `127.0.0.1`. * **Domain capacity**: Each project can register up to 25 allowed embed domain origins per mode (Sandbox and Live). * **The two-key system**: Registering your origin in the embed domain allowlist is required, but it is only half of enabling embedded checkout. The payment button itself **ALSO** needs **Open over my own page** ticked in its button settings. Both conditions must be met for the frame to render. ## Paying with a wallet eSewa and Khalti authenticate inside their own app or website. That is what keeps the customer's PIN away from your site and from ours, so the overlay hands off rather than trying to contain it. * **On a phone**, the wallet opens in place and returns to the checkout when it is done. * **On a desktop**, the wallet may open in a separate window that closes itself once the payment lands. The overlay shows a waiting state until it does. * **In an in-app browser** (a link opened inside Instagram, Facebook, or a similar app), the wallet always opens in place. Separate windows are unreliable there. Fonepay is the exception: its QR is displayed and confirmed inside the overlay with no hand-off at all. While the customer is away, the overlay shows a waiting screen with a manual status check. If a payment completes but the provider's confirmation never reaches us, that check asks the provider directly rather than only re-reading our own records. ## Closing and reopening is safe If the customer closes the overlay and opens it again, they resume **the same payment**, not a new one. Anything they had already typed is still there. This matters for more than convenience: it means one order can never produce two live payment sessions, so a customer cannot accidentally pay twice for the same thing. ## PayBridgeNP ID inside the overlay [PayBridgeNP ID](/guides/paybridgenp-id) works in the overlay, with one difference worth planning around. **What works.** A returning customer types their mobile number, we tell them we have their details, they enter a one-time code, and their name, email and phone fill in. Same as hosted checkout. **What does not.** They are not recognised *before* they type. On hosted checkout a returning customer is greeted by name on arrival; in the overlay they always start by entering their number. The reason is the browser, not a missing feature. Silent recognition depends on a cookie on the PayBridgeNP domain, and browsers deliberately withhold that cookie from a page running inside another site. The overlay is exactly that. For the same reason, "remember me on this device" is offered only on hosted checkout. The overlay does not show it, because the grant it creates could not be delivered. ## What you still handle The overlay is the payment step only. Your cart, your prices, your discounts, and your order records stay yours. PayBridgeNP creates a session for an amount you give it and tells you when that amount is paid. Discount codes are a good example: apply them in your own cart and send us the final amount. There is no promo-code field in the checkout. ## Verifying the payment Do not treat the overlay closing as proof of payment. Confirm it the same way you would for any other PayBridgeNP integration: * Listen for the [`payment.succeeded` webhook](/guides/webhook-verification), which is signed and retried until you acknowledge it. * Or read the session back through the API. See [Confirming payments](/guides/confirming-payments) for the full contract. ## Limits * Each button opts in separately. A button you have not ticked keeps redirecting. * The overlay renders only on allowed origins registered for your project in the active mode. * Wallet payments briefly leave the overlay, by design. * Returning customers are not recognised until they enter their mobile number. Hosted Checkout greets them on arrival; the overlay cannot. * A button's amount is fixed, so a button suits a fixed price. Never drive a cart total from a custom-amount button: it takes its amount from the page, which means the browser can change it. For carts and per-product prices, have your server create the session with `embedOrigin` instead, as described in [Cart totals and per-product prices](#cart-totals-and-per-product-prices). * Platform plugins (WooCommerce, Shopify) use Hosted Checkout and cannot open the overlay. # eSewa Intent Source: https://docs.paybridgenp.com/guides/esewa-intent Mobile-app deeplink + cross-device QR for eSewa payments. Buyer pays inside the eSewa app or by scanning a QR with their phone, never leaves your checkout. eSewa Intent is a newer payment flow from eSewa that lets buyers pay inside the eSewa mobile app instead of the eSewa web page. PayBridgeNP handles both the deeplink (mobile) and the cross-device QR (desktop) automatically - same `POST /v1/checkout` call, eSewa just feels faster and more native. Intent is **live**. In **sandbox** it is open to every plan so you can prove the integration first. In **live** it is a **Pro** feature and each merchant supplies their own eSewa production Intent Access Key (see [Going live](#going-live)). ## Why Intent matters Compared to the existing eSewa v2 ePay (web) flow, Intent gives you: | | v2 ePay (web) | Intent | | ---------- | ----------------------------------------- | ----------------------------------------- | | Mobile UX | Browser detour, retype password, OTP | One tap, native app, biometric | | Desktop UX | Browser redirect to eSewa.com.np | QR code on your checkout, scan with phone | | Conversion | Baseline | Higher on mobile (less drop-off) | | Brand feel | "I left your store and went to a website" | "I tapped pay and it just worked" | Most NP commerce is mobile-heavy. Intent removes the web detour for the majority of your buyers, which usually shows up as a measurable conversion lift. ## How it works You don't change anything about how you create checkout sessions. PayBridgeNP picks Intent automatically when: 1. The merchant has eSewa configured (existing `merchantCode` + `secretKey`), AND 2. The merchant has flipped the **eSewa Intent** toggle on at *Dashboard → Providers → eSewa* (off by default), AND 3. The merchant's plan allows Intent - any plan in **sandbox**, **Pro** in **live**, AND 4. In **live** only: the merchant has supplied their own eSewa production **Intent Access Key** (sandbox auto-uses eSewa's public test key, so no key setup is needed there). If any of these fail, the existing v2 ePay flow runs instead - buyer sees no difference. ### What the buyer sees Same picker tile as before. No "Intent vs Web" choice - we route automatically. The page fires the eSewa deeplink. The eSewa app launches with the payment pre-filled. Buyer authenticates with biometric / PIN and pays. If the app does not open within 3 seconds (e.g. the buyer does not have the eSewa app installed), the page silently submits the v2 ePay web form so the buyer never gets stuck. The page renders a QR encoding the same deeplink. Buyer scans it with the eSewa app on their phone, pays, and the desktop page polls our `/status` endpoint and flips to "Payment received" within seconds. There is also a "Pay on the web instead" link for buyers who do not have the eSewa app on their phone. PayBridgeNP verifies the payment with eSewa and sends your existing signed payment webhook. ### What changes on your side Nothing in the API. Your existing `POST /v1/checkout` request and `payment.succeeded` webhook stay the same. The provider remains `"esewa"`. ## Testing in sandbox In sandbox mode, **any merchant on any plan** can test Intent. You do not need to upgrade. The dashboard auto-fills eSewa's public sandbox merchant code, secret key, and Intent access key for you - no setup beyond flipping the toggle. To test the full flow you will need: 1. A sandbox merchant account on PayBridgeNP (any plan). 2. **eSewa Intent toggled on** at *Dashboard → Providers → eSewa* (off by default - flip it to enable Intent for your sandbox). 3. eSewa's **sandbox test app** (UAT APK). Direct download from eSewa: [esewa\_rc\_release.apk](https://gitlab.com/esewa-pub/esewa-intent-payment-app/-/raw/main/esewa_rc_release.apk?ref_type=heads) (\~58 MB). Install it on an Android device alongside your real eSewa app - the UAT app is a separate package and points at eSewa's sandbox backend, so live payments cannot leak through it. 4. A phone on the same WiFi as your dev server (for the desktop QR scan flow). Or just open the deeplink URL on the phone directly. Then create a session normally: ```bash theme={null} curl -sS -X POST https://api.paybridgenp.com/v1/checkout \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "amount": 1000, "currency": "NPR", "description": "Test payment", "returnUrl": "https://yourstore.com/done", "customer": { "name": "Test Customer", "email": "test@example.com", "phone": "+9779800000001" } }' ``` Open the returned `checkout_url` and pick eSewa. Mobile gets the deeplink; desktop gets the QR. ## Refunds Refunds for Intent payments behave the same as v2 ePay: PayBridgeNP records the refund as `requires_action` and returns a message asking you to process it manually through the eSewa merchant dashboard. ## Going live Live Intent is open. To turn it on for real payments, a merchant needs all of: 1. A **Pro** (or Enterprise) plan - `esewa.intent` is a Pro entitlement in live. 2. Their own eSewa production **Intent Access Key**, requested from eSewa and pasted into *Dashboard → Providers → eSewa* (the sandbox public test key is rejected in live). 3. The **eSewa Intent** toggle on for eSewa. With all three in place, live buyers get the Intent deeplink and QR. A live merchant who has not configured their Intent Access Key (or is not on Pro) keeps getting the v2 ePay flow automatically, so nothing breaks. ## Frequently asked **Will my existing eSewa integration keep working?** Yes. Intent is additive - v2 ePay remains the automatic fallback whenever Intent does not apply (a merchant not configured for live Intent, or a buyer without the eSewa app installed). **Do I need to change my SDK / webhook code?** No. The provider is still reported as `"esewa"`, and the public payload shape is unchanged. **What happens if the buyer does not have the eSewa app installed?** On mobile, the page detects the deeplink did not open the app (3-second timeout via `visibilitychange`) and auto-submits the v2 ePay web form so the buyer continues without friction. On desktop, the QR is the primary call to action and there is a "Pay on the web instead" link beside it. **Can buyers refund themselves?** No. Refunds are merchant-initiated, same as today. **Why is there a 30-minute session expiry?** Buyers may background the eSewa app (phone call, app switch) and come back later. 30 minutes matches the picker's expiry and gives buyers cushion. eSewa's own session window is shorter; if they expire on eSewa's side first the buyer gets a clear "session expired" message and can restart from your store. # Idempotency Source: https://docs.paybridgenp.com/guides/idempotency Make retries safe - avoid duplicate payments when network failures occur. Network failures happen. If your server sends a `POST /v1/checkout` request and the connection drops before you receive a response, you don't know whether PayBridgeNP created the session or not. Retrying blindly can create duplicate checkout sessions for the same order. PayBridgeNP supports idempotency keys on `POST /v1/checkout` to make retries safe. ## How it works Pass a unique `Idempotency-Key` header with your request: ```bash theme={null} curl -X POST https://api.paybridgenp.com/v1/checkout \ -H "Authorization: Bearer sk_test_your_key" \ -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \ -H "Content-Type: application/json" \ -d '{ "amount": 10000, "returnUrl": "https://yoursite.com/success" }' ``` If the request succeeds, PayBridgeNP stores the key and response. If you retry with the **same key**, PayBridgeNP returns the original response without creating a new session - even if the original request completed successfully. ## Key rules * **Use UUID v4** - random, unguessable, one per logical operation (not per HTTP request) * **Keys are scoped to your project and live or sandbox mode** * **Responses are retained for at least 24 hours** - never reuse a key for a different operation * **Limit refusals are not stored** - a request refused because of a plan limit (`402`), a feature your plan does not include, a rate limit (`429`), or a spend cap is evaluated again when you retry with the same key, so it can succeed once the limit clears. Other error responses, such as validation errors, are returned again as they were * **Supported on the write endpoints that create resources** - `POST /v1/checkout`, `POST /v1/refunds`, `POST /v1/webhooks`, and the billing POSTs (plans, customers, subscriptions, invoices, and their actions). GET requests are idempotent by design and ignore the key ## Generating a UUID v4 ```typescript Node.js / Bun theme={null} import { randomUUID } from "crypto"; const idempotencyKey = randomUUID(); // "550e8400-e29b-41d4-a716-446655440000" ``` ```typescript Browser theme={null} const idempotencyKey = crypto.randomUUID(); ``` ```php PHP theme={null} $idempotencyKey = \Ramsey\Uuid\Uuid::uuid4()->toString(); // or: bin2hex(random_bytes(16)) ``` ```python Python theme={null} import uuid idempotency_key = str(uuid.uuid4()) ``` ## Using with the SDK ```typescript theme={null} import { randomUUID } from "crypto"; import { PayBridgeNP } from "@paybridge-np/sdk"; const paybridgenp = new PayBridgeNP({ apiKey: process.env.PAYBRIDGENP_API_KEY! }); // Generate the key once and store it with your order const idempotencyKey = randomUUID(); await db.orders.update({ idempotencyKey }, { where: { id: orderId } }); const session = await paybridgenp.checkout.create( { amount: 10000, returnUrl: "https://yoursite.com/success", metadata: { orderId }, }, { idempotencyKey }, ); ``` On retry, pass the **same key** that was stored with the order: ```typescript theme={null} const order = await db.orders.findOne({ where: { id: orderId } }); const session = await paybridgenp.checkout.create( { amount: order.amount, returnUrl: "...", metadata: { orderId } }, { idempotencyKey: order.idempotencyKey }, // same key = same response ); ``` ## Concurrent request conflict If two requests with the **same key** arrive at exactly the same time, one of them will receive a `409 Conflict`: ```json theme={null} { "error": { "message": "Concurrent request with same Idempotency-Key", "type": "idempotency_error", "code": "idempotency_conflict", "request_id": "req_..." } } ``` This is safe to retry after a short delay - it means a parallel request is already in flight. ## When to use idempotency keys | Scenario | Use? | | ------------------------------------------------- | --------------------------------------------------- | | Server → PayBridgeNP API call on order submission | **Yes** | | Client-side button that could be clicked twice | **Yes** - generate key server-side | | Automated retry logic in your HTTP client | **Yes** | | Polling `GET /v1/payments/:id` | No - GET is already safe to retry | | Webhook handlers | No - use webhook deduplication on your side instead | # Metered Billing Source: https://docs.paybridgenp.com/guides/metered-billing Charge customers based on actual usage with metered plans. Metered billing lets you charge customers based on what they consume in a billing period - API calls, active seats, storage used, or any other unit you define. At the end of each period, PayBridgeNP aggregates the usage records and issues an invoice for the accumulated amount. ## Create a metered plan ```typescript theme={null} const plan = await paybridgenp.plans.create({ name: "API calls", amount: 50, // unit price: 50 paisa per call intervalUnit: "month", billingScheme: "metered", aggregationMethod: "sum", // sum | max | last_ever }); ``` `billingScheme` is set at creation and cannot be changed afterwards - switching a plan between `per_unit` and `metered` under live subscriptions would change what their invoices bill. Create a new plan instead. A metered plan is defined by two settings: * **`billingScheme: "metered"`** - bill on reported usage instead of a flat price * **`aggregationMethod`** - how usage is combined over the billing period: * **`sum`** - total usage across all records in the period (default) * **`max`** - the single highest usage value reported (useful for high-water-mark billing) * **`last_ever`** - the most recent value ever reported The plan `amount` is the **unit price** in paisa. The invoice is `amount × aggregated_quantity`. ## Report usage Call `reportUsage` after each usage event, or batch at intervals: ```typescript theme={null} await paybridgenp.subscriptions.reportUsage(subscriptionId, { quantity: 100, // number of units consumed action: "increment", // "increment" (default) or "set" idempotencyKey: "evt_abc123", // prevents double-counting on retry }); ``` Use `action: "set"` to report the absolute current value instead of adding to the running total (useful with `max` or `last_ever` aggregation). ## Check current usage ```typescript theme={null} const summary = await paybridgenp.subscriptions.getUsageSummary(subscriptionId); // { quantity: 1500, record_count: 15, aggregation_method: "sum", period_start, period_end } ``` ## How invoicing works At the end of each billing period, PayBridgeNP: 1. Aggregates all usage records for the period using the plan's `aggregationMethod` 2. Multiplies the result by the plan's `amount` (unit price) 3. Generates an invoice for that total 4. Resets the usage counter for the next period If no usage was reported in a period, a zero-amount invoice is generated and immediately marked as paid. ## Per-seat billing For per-seat (quantity-based) billing where you charge a flat rate multiplied by the number of seats, use `billingScheme: "per_unit"` (the default) and update the quantity when seats change: ```typescript theme={null} // Create per-unit plan const plan = await paybridgenp.plans.create({ name: "Team Plan", amount: 149900, // Rs. 1,499 per seat per month intervalUnit: "month", // billingScheme: "per_unit" is the default }); // Create subscription with initial seat count const sub = await paybridgenp.subscriptions.create({ customerId, planId: plan.id, quantity: 5, // 5 seats = Rs. 7,495/month }); // Update when seats change await paybridgenp.subscriptions.updateQuantity(sub.id, 8); ``` # PayBridgeNP ID Source: https://docs.paybridgenp.com/guides/paybridgenp-id Buyer-side identity baked into hosted checkout. Returning shoppers autofill across every PayBridgeNP merchant with zero merchant integration. PayBridgeNP ID (PB ID) is the buyer-identity layer that ships inside hosted checkout. Once a buyer has saved their info at one PayBridgeNP merchant, every other PayBridgeNP merchant gets the same buyer recognized, autofilled, and converted. You don't integrate anything. PB ID surfaces on the checkout page you already use the moment a returning buyer lands on it. ## The buyer's first save After their first paid checkout, the buyer sees a "Save with PayBridgeNP ID" prompt on the success page. One SMS code stores their name, email, address, and preferred wallet. Tapping "No thanks" sets a 90-day cookie so we don't ask again from that browser. That's the entire signup. There is no separate registration page, no password, no app to install. ## Recognizing returning buyers PB ID has three recognition paths, all on the same hosted checkout page: ### Trusted device (silent autofill) When a buyer is signed in on `id.paybridgenp.com`, hosted checkout can recognize the trusted device and fill the saved profile after matching it to the checkout. The buyer sees a welcome message, chooses a payment method, and pays without another sign-in. This is the lowest-friction path and it is the default for anyone who has signed in once on the buyer site. ### Phone match (State A) If the merchant pre-filled `customer.phone` on the checkout session (or the buyer types it in the cold form), we look up an existing PB ID by hashed phone. If we find one, the page shows: > Welcome back to PayBridgeNP > Send a code to 98XXXXX001 to fill in your details > \[Send code] \[Use a passkey] \[Use a different account] The buyer either gets an SMS, taps Touch ID for an instant passkey, or chooses to type fresh details. ### Cold (State C) For buyers we have no signal on (no cookie, no matching phone), the form shows as normal with a small "Already on PayBridgeNP? Sign in with a passkey" pill above the inputs. The buyer can ignore it and type, or tap to sign in via discoverable passkey. ## Sign-in methods | Method | Where it works | Notes | | ----------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------- | | SMS one-time code | Login, checkout, step-up | 6 digits, valid 15 minutes | | Passkey (Face ID / Touch ID / device PIN) | Login, checkout, step-up, account dashboard | Buyer registers once at id.paybridgenp.com, then signs in everywhere with a tap | ## Sensitive-action protection Deleting an account or downloading a data export prompts the buyer for fresh verification with an SMS code or passkey. The verification is **single-use**: each gated action requires its own step-up, so one OTP cannot unlock multiple destructive actions in a row. ## What the merchant sees Nothing changes on the merchant side. Autofilled name, email, phone, and address use the same checkout and webhook customer fields as a guest payment. The `payment.succeeded` event is unchanged. The buyer's `merchants` tab on `id.paybridgenp.com` lists every PayBridgeNP merchant they've paid, with a per-merchant "remove" button. Revoking a merchant removes that merchant link without affecting the historical payments. ## What gets stored, what does not **Stored:** * Phone number and email, encrypted at rest * Name and default shipping address, encrypted at rest * Preferred wallet, which is not a payment credential * Passkey public keys + counter (private half stays on the buyer's device) * Buyer-merchant links and account activity **Not stored:** * eSewa, Khalti, or Fonepay passwords. The buyer always logs in with the wallet on the wallet's own page. * Card numbers (PayBridgeNP doesn't accept cards). * IP-derived location. * Any merchant's order details beyond what that merchant already sees. ## Data export and deletion Buyers can export a JSON copy of their PayBridgeNP ID data from `id.paybridgenp.com`: profile, merchant links, payments and refunds, checkout sessions, devices, passkeys, Shop orders, imported Insights data, Wrapped, and the full audit log. Passkey entries are metadata only, never the credential id or public key, and a duplicate charge recovered directly from a provider carries no checkout link so it is not listed. Account deletion is gated by a fresh OTP, soft-deletes immediately (revoking all sessions and identity lookup), and hard-deletes after 30 days. Hard-delete removes the encrypted ciphertexts and breaks every merchant link. Shop orders are anonymised rather than destroyed: recipient details, gift messages and voucher codes are erased, while the amount, status and dates survive as a financial record. Payments the buyer made to merchants are also retained, unlinked from the deleted identity, because those are the merchant's own transaction records. ## Languages The buyer site (`id.paybridgenp.com`) runs in नेपाली and English. Login detects the buyer's preference automatically. The hosted checkout banners (`Welcome back...`, `Send code`, etc.) inherit the merchant's checkout theme strings; merchant-side localisation will arrive in a future release. ## Privacy and security posture * Per-merchant data isolation: each merchant only sees the buyers who've paid them. * Buyer-revocable links: a single tap on `id.paybridgenp.com` removes a merchant from the buyer's history. * Protected sessions and fresh verification for sensitive actions. * WebAuthn ceremonies use discoverable credentials with `userVerification=preferred`. Counter-rollback is enforced where the authenticator supports monotonic counters. * Sub-processor list and retention timeline live in the [privacy policy](https://paybridgenp.com/privacy). ## FAQ **Do I need to call any new API to enable PB ID?** No. PB ID is on by default, and the hosted checkout surfaces it automatically. There is no SDK call and nothing to integrate. If you want it off, flip the per-project toggle described below. **Why don't I see PB ID on my sandbox checkout?** By design, not a bug. PB ID is live-mode only: a sandbox checkout session renders the plain customer form, with no "Welcome back" banner, no save-after-pay prompt, and no passkey pill, and nothing is written to a buyer's real PayBridgeNP ID from a sandbox payment. PB ID lives entirely inside hosted checkout with zero merchant integration, so there is nothing for you to test against it in sandbox, and keeping sandbox out of it keeps test traffic from touching real buyer identities. To see the PB ID experience yourself, create a live-mode session in a test project, or sign in directly at id.paybridgenp.com. **Can I disable PB ID on my checkout?** Yes, per project. In the dashboard, open your project's settings, go to Branding, and turn off "PayBridgeNP ID on checkout". Disabling it suppresses the "Welcome back" banner, the save-after-pay prompt, and trusted-cookie autofill on that project's checkouts, so buyers see a normal cold checkout form. Identities buyers have already saved stay valid and keep working at other merchants and projects. **What happens if the buyer's cookie identity has a different phone than the one I pre-filled?** The trusted-cookie path refuses to autofill, the page falls back to normal State A or State C, and the buyer can sign in with the matching account or pick "Use a different account" to type fresh details. **Does PB ID work on my mobile SDK or my Shopify app?** PB ID is currently a hosted-checkout feature. Mobile SDK and Shopify pay-by-link checkouts go through the same hosted page, so they benefit automatically when the buyer arrives in a browser. Native popup or in-app modal flows do not yet surface PB ID. # Payment Buttons Source: https://docs.paybridgenp.com/guides/payment-buttons Add a checkout button to any website with a single script tag - no backend required. Payment buttons add a configured checkout button to any website without a backend integration. Create one in the [dashboard](https://dashboard.paybridgenp.com) and paste its ` ``` The button renders inline, right where the script tag is placed. No additional CSS or JavaScript is required. The snippet only contains a button ID. All configuration (label, color, amount, collect fields) is fetched live from the API on each page load. If you change a setting in the dashboard, every page using that button updates automatically - you never need to re-embed. ### Multiple buttons on one page You can embed multiple buttons on the same page. Each script tag renders independently: ```html theme={null} ``` ### Inline overrides In rare cases, you may want the same button to behave differently on different pages (e.g. different amounts for different products). You can override settings with `data-*` attributes on the script tag: ```html theme={null} ``` | Attribute | Description | | --------------------------- | ------------------------------------------------------------ | | `data-amount` | Override the amount (in paisa, e.g. `150000` = NPR 1,500.00) | | `data-label` | Override the button label text | | `data-color` | Override the button color (hex, e.g. `#1e00ff`) | | `data-collect-name` | Override collect name setting (`"true"` or `"false"`) | | `data-collect-email` | Override collect email setting (`"true"` or `"false"`) | | `data-collect-phone` | Override collect phone setting (`"true"` or `"false"`) | | `data-customer-name` | Prefill the customer's name | | `data-customer-email` | Prefill the customer's email | | `data-customer-phone` | Prefill the customer's phone | | `data-customer-line1` | Prefill address line 1 (needs `data-customer-city` too) | | `data-customer-city` | Prefill the city (needs `data-customer-line1` too) | | `data-customer-line2` | Prefill address line 2 | | `data-customer-state` | Prefill the province or state | | `data-customer-postal-code` | Prefill the postal code | | `data-customer-country` | Prefill the country (defaults to Nepal) | Inline overrides take precedence over dashboard settings. If an attribute is not present, the dashboard setting is used. ## Prefilling for a customer who is already signed in If your site already knows who the shopper is, pass it and they will not retype it: ```html theme={null} ``` Render those values from your own session, the same way you would prefill any other form on your site. A few things worth knowing: * **The customer can still edit every field.** These are a starting point, not a lock. A stale entry in your customer table has to be fixable at the moment of payment. * **A value we cannot use is dropped, not fatal.** An email that does not parse means the customer types that one field. It never fails the checkout. * **Only fields the button collects are used.** A button that does not ask for a phone ignores `data-customer-phone`. * **An address needs line 1 and the city together.** Either both or neither is used, because that is the pair we treat as a usable address. Sending half of one would look like it worked and then quietly vanish. * **This is not a sign-in.** Prefilled details are treated exactly like typed ones. They never link a [PayBridgeNP ID](/guides/paybridgenp-id) or mark contact details as verified; that still needs a one-time code from the customer. These values are read from your page, so treat them as a convenience for the customer rather than a record of who paid. The payment record is what your webhook and the API return, not what the page sent. ## Customer experience When a customer clicks the button: 1. A hosted button opens a short pre-checkout form. It collects configured fields and, for a custom-amount button, the amount. A fixed-amount [Embedded Checkout](/guides/embedded-checkout) button opens the overlay directly. 2. PayBridgeNP creates the checkout session and shows the available payment methods. 3. The customer completes eSewa, Khalti, or Fonepay. 4. After payment, the customer returns to your return URL with `session_id`, `status`, and `payment_id` query parameters. ### Return URL parameters After payment, your return URL receives these query parameters: ``` https://yoursite.com/thank-you?session_id=cs_xxx&status=success&payment_id=pay_xxx ``` | Parameter | Description | | ------------ | ---------------------------------------- | | `session_id` | The checkout session ID | | `status` | `success` or `failed` | | `payment_id` | The payment ID (only present on success) | Do not rely solely on the return URL parameters to confirm payment. A customer can modify the URL. Always verify payment status server-side with a [webhook](/guides/webhook-verification). See [Confirming a payment](/guides/confirming-payments) for the full pattern. ## Validation The payment button validates customer input before submitting: | Field | Validation | | --------------- | -------------------------------------------- | | Full name | At least 2 characters | | Email | Valid email format (e.g. `user@example.com`) | | Phone | 7-15 digits, optional `+` prefix | | Amount (custom) | Positive number in NPR | These rules are enforced in the pre-checkout form when it is shown and again by the API. Invalid input will not create a checkout session. ## Webhooks Payment button transactions fire the same webhooks as API-created checkout sessions. The payload is identical - it does not carry a dedicated payment-button identifier. Set up a webhook endpoint in the dashboard to receive `payment.succeeded` events. See [Webhook verification](/guides/webhook-verification) for details. ## Managing buttons ### Enable / disable Toggle a button on or off from the dashboard table. Disabled buttons are invisible - the script tag renders nothing on your page, so visitors see a blank space instead of a broken button. ### Editing Click the edit icon on any button to change its settings. Since the button fetches config live, changes take effect on the next page load. No action is needed on your website. ### Deleting Deleting a button is permanent. Any pages still using its embed snippet will render nothing. ### Stats The dashboard table shows per-button stats: | Column | Description | | ------------ | ------------------------------------------------- | | **Payments** | Number of successful payments through this button | | **Volume** | Total NPR amount collected through this button | ## Sandbox testing Payment buttons are scoped to the API key used to create them. A button created with a `sk_test_…` key uses sandbox credentials and test payment flows. A button created with a `sk_live_…` key processes real payments. See [Sandbox testing](/guides/sandbox-testing) for provider-specific test credentials. ## Limitations * Payment buttons are created and managed from the dashboard only - there is no API to create them programmatically * Each project can have up to 200 buttons * The button script requires JavaScript to be enabled in the customer's browser * The button renders as an inline-block element with a max width of 340px * Custom CSS cannot be applied to the button (styling is self-contained to prevent conflicts with your site) # Payment Links Source: https://docs.paybridgenp.com/guides/payment-links Create no-code hosted payment pages and share them with customers. Payment links are shareable URLs that let customers pay without any code on your side. Each link points to a hosted PayBridgeNP checkout page - you configure the amount, description, and expiry in the [dashboard](https://dashboard.paybridgenp.com), then share the URL anywhere. ## Use cases * **Invoicing** - send a payment link in an email or SMS * **Donations** - embed a payment link on a website or social media * **Event tickets** - sell a fixed number of seats by setting a max use limit * **Subscriptions alternative** - recurring manual billing for small businesses ## Creating a payment link 1. Go to **Payment Links** in the [dashboard](https://dashboard.paybridgenp.com) 2. Click **New Payment Link** 3. Configure the link: | Field | Required | Description | | ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Amount** | Yes | Fixed amount in NPR (e.g. Rs. 500) | | **Title** | Yes | Shown on the checkout page | | **Description** | No | Additional details for the customer | | **Reference ID** | No | Your internal reference (e.g. `invoice_123`) - used for tracking | | **Expiry** | No | Date after which the link stops working | | **Max uses** | No | Maximum number of successful payments (e.g. `50` for event tickets) | | **Provider** | No | Pre-select a payment provider, or let the customer choose | | **Collect shipping address** | No | When enabled, customers fill in their address before paying. The address is included in the `payment_link.paid` webhook payload as `customer_address`. | 4. Click **Create** - PayBridgeNP generates a URL like: ``` https://paybridgenp.com/pay/lnk_... ``` ## Tracking payments Payments made through a link fire webhooks (`payment_link.paid` and `payment.succeeded`), which are the reliable way to know a link has been paid. See [Confirming a payment](/guides/confirming-payments) for the webhooks-first pattern and how to verify a payment server-side. ## Webhooks Payment link payments fire a `payment_link.paid` event (and also `payment.succeeded`). The webhook payload includes the `metadata` attached to the payment link, a `payment_link_id` field, and - when address collection is enabled - a `customer_address` object. Set up a webhook endpoint in the dashboard to receive these events. ## Link lifecycle | Status | Meaning | | ----------------- | ------------------------------------------------- | | **Active** | Accepting payments | | **Expired** | Past the expiry date - no longer accepts payments | | **Limit reached** | Max uses hit - no longer accepts payments | | **Disabled** | Manually turned off from the dashboard | You can disable or re-enable a link at any time from the dashboard. ## Creating links programmatically Payment links have a full API - see [Create a payment link](/api-reference/payment-links/create-payment-link). Beyond a fixed `amount`, a link can accept an open amount within a range (`minAmount`/`maxAmount`) so the customer enters what they pay, and it can send the customer to your own page after payment with `redirectUrl`. You can also create and manage links from the dashboard. ## Limitations * A link's provider set and currency (NPR) are fixed once created # Sync with provider Source: https://docs.paybridgenp.com/guides/payments-sync Pull the latest status of a Khalti or eSewa payment directly from the provider, and reconcile out-of-band refunds issued from the provider's own merchant dashboard. If you issue a refund from inside the Khalti or eSewa merchant dashboard (instead of from PayBridgeNP), the customer is refunded but PayBridgeNP doesn't know about it. Your payment row stays marked "Succeeded", your reports overstate revenue, and any downstream system that listens to `payment.refunded` webhooks never gets notified. **Sync with provider** fixes that. It re-asks the provider what they currently think about a specific payment, and reconciles your PayBridgeNP record if the provider's view has changed. Sync is a read action followed by a conservative write. It will create a refund row and fire a webhook only when the provider's signal is unambiguous (a full refund). Anything ambiguous gets flagged for you to review, never silently flipped. ## When to use it The most common reason: **a refund was issued outside PayBridgeNP**. * You logged into your Khalti merchant dashboard and pressed Refund there. * You logged into your eSewa merchant panel and pressed Refund there. * Khalti or eSewa support reversed a transaction on your behalf. * A partner system on your end issued the refund through its own integration with the provider, bypassing PayBridgeNP. Other useful cases: * A payment shows as Succeeded in PayBridgeNP but you want to confirm the provider still agrees (for example, before sending a high-value receipt). * A customer disputes a payment and you want a fresh provider-side state on file before responding. ## Where to find it On any individual payment's detail page in your dashboard (**Payments → click a row**), you'll see a **Sync with provider** button next to "Issue refund" and "Resend receipt". The button only appears when: * The provider is **Khalti** or **eSewa**. * The payment status is **Succeeded** or **Refunded**. For other providers and other states, the button is hidden because the action wouldn't do anything useful. ## What gets synced PayBridgeNP calls the provider's status endpoint: | Provider | Endpoint | Identifier used | | -------- | ----------------------------------------- | ------------------------------------------------------------------------------ | | Khalti | `POST /epayment/lookup/` (Khalti ePay v2) | `pidx` stored when the customer first checked out | | eSewa | `POST /api/epay/transaction/status/` | `transaction_uuid` + `total_amount` stored when the customer first checked out | The provider returns a status string (and a few related fields). PayBridgeNP maps that into one of seven states and decides what to do. ## What happens for each provider state | Provider state | What PayBridgeNP does | Why | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `succeeded` | **No change.** Toast: "Provider confirms the payment is still valid." | The provider agrees with your record. Sync is idempotent so you can press it any number of times without side effects. | | `refunded_full` | **Records a refund row** for the full remaining amount with source `provider_sync`, flips the payment to **Refunded**, fires a `payment.refunded` webhook. | A full refund is a clean, unambiguous signal. The refund note reads "Detected via provider sync - refund issued outside PayBridgeNP." | | `refunded_partial` | **Flagged for review.** No state change, no refund row. Toast asks you to record the refund manually. | Neither Khalti nor eSewa expose the refunded amount in their lookup response, only a "partially refunded" boolean. We won't guess an amount. | | `cancelled` | **Flagged for review.** No state change. | A cancelled state arriving on a payment we already consider succeeded usually means the provider's view drifted, not that your money disappeared. We don't auto-flip a successful payment on a single suspicious response. | | `expired` | **Flagged for review.** No state change. | Same reasoning as cancelled. | | `not_found` | **Flagged for review.** No state change. | If the provider can't find the transaction, we want a human to investigate before deleting anything. | | `pending` / `ambiguous` | **Flagged for review.** No state change. | Indeterminate signal. Press Sync again later, or contact provider support if it persists. | The "needs review" outcomes show up as a yellow toast in the dashboard. They never delete or reverse a successful payment automatically. ## What the merchant sees You'll get one of three toasts after pressing the button: * **Success (green)** - "Provider reported a full refund. Recorded refund of NPR X." The payment row reloads as Refunded, the refund appears in your Refunds list with source `provider_sync`, and your `payment.refunded` webhook fires. * **Info (gray)** - "Provider confirms the payment is still valid." Nothing changed. * **Warning (yellow)** - "Provider reports … Investigate before mutating." Read the message, decide what to do, and either record a manual refund or contact support. ## Webhook behavior When Sync detects a full refund and creates a refund row, PayBridgeNP fires the standard `payment.refunded` webhook to every endpoint registered for the project. The payload includes `"source": "provider_sync"` so your downstream systems can distinguish reconciled refunds from refunds you issued through the API or dashboard. If your webhook handler treats provider-sync refunds differently (for example, suppressing a customer-facing email because the customer already saw the provider's confirmation), you can branch on that field. For the partial / cancelled / expired / not-found / ambiguous outcomes, no webhook fires, because no state change occurred. ## Audit log Every press of the Sync button is recorded in the audit log, regardless of whether anything changed. The audit row carries: * The user who pressed it * The provider's reported state * The outcome (`refund_recorded`, `no_change`, or `needs_review`) * The refund ID and amount, when applicable This is so that when a customer asks "who refunded me?" or a teammate asks "why did this flip to Refunded?", the answer is one query away. ## Limitations * **Fonepay is not supported.** Fonepay's confirmation flow is WebSocket-based and has no equivalent status endpoint. Refunds issued out-of-band from Fonepay must be reconciled manually. * **Partial refunds need manual entry.** Khalti and eSewa both omit the refunded amount from their lookup responses. PayBridgeNP can tell you a partial refund happened, but not for how much. * **Pre-feature payments cannot sync.** Sync needs the provider's lookup identifier (Khalti `pidx` or eSewa `transaction_uuid` + `total_amount`) to have been stored at the time the payment was created. Payments created before this feature shipped don't have those identifiers and will return an error advising you to record any out-of-band refund manually. * **Sync is manual, not scheduled.** PayBridgeNP does not poll providers in the background. If you regularly issue refunds from the provider's dashboard, you'll need to remember to press Sync afterwards (or just issue refunds from PayBridgeNP, which avoids the problem entirely). * **One payment at a time.** There's no bulk sync. If you've issued many out-of-band refunds, contact support and we can run a one-off reconciliation. ## Best practice The simplest way to never need this feature: **issue refunds from PayBridgeNP, not from the provider's dashboard**. PayBridgeNP's refund flow calls the provider for you, records the refund row, fires the webhook, and updates the payment status in one atomic step. Sync exists for the times that's not possible (provider support reversed something, an external system bypassed your normal flow, or a teammate reached for the provider dashboard out of habit). ## Errors | Error | Meaning | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Sync is not available for fonepay` | Fonepay has no status endpoint. Reconcile manually. | | `khalti credentials not configured for this project` | The provider credentials were removed after this payment was created. Re-add them in **Settings → Providers** to enable sync. | | `No pidx stored for this payment` (Khalti) | Pre-feature payment. Record the refund manually. | | `eSewa requires both transaction_uuid and total_amount` | Pre-feature payment. Record the refund manually. | | `Sync failed` | The provider's status endpoint returned an error or timed out. Press Sync again in a moment. The most recent state in your dashboard is still authoritative until a successful sync overrides it. | # Per-Project Branding Source: https://docs.paybridgenp.com/guides/per-project-branding Override your merchant-level logo, theme, and receipt branding on a per-project basis. For multi-brand merchants running several stores from one PayBridgeNP account. By default, every project under your PayBridgeNP account inherits its visual branding (logo, checkout theme, receipt logo, brand color, support email, custom message, footer text) from the merchant-level settings at **Settings → Emails**. That's the right setup for single-brand merchants: configure once, every checkout and receipt looks the same. If you run multiple brands from the same PayBridgeNP account, per-project branding lets you override any field on a specific project, so each brand's customers see that brand's identity end-to-end. Per-project branding is available on every plan. The custom-domain feature it pairs with (`pay.brandA.com`, `pay.brandB.com`) is a Pro-plan feature. ## When to use it Use it when: * You run more than one storefront from one PayBridgeNP account (e.g. `glamora`, `nepalbrews`, `arctic-shop` as three separate Shopify stores all routing payments through PayBridgeNP). * Each storefront should send receipts that match its own branding, not your shared account name. * You want different checkout-page logos and themes per project. Skip it when: * You're a single-brand merchant. The merchant-level settings at **Settings → Emails** already cover every project automatically. * You only want different colors per project but don't have separate brands. (You can still use it, just less impactful.) ## What you can override per project | Field | Where it shows | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | Logo | Hosted checkout page (above the payment buttons) | | Checkout theme | Hosted checkout page (clean-v2) | | Receipt logo | Customer receipt emails, refund notification emails, downloadable PDF receipts, buyer activity feed at `id.paybridgenp.com` | | Brand color | Hosted checkout page (buttons, links, selected payment method), customer receipt + refund emails (top accent bar), downloadable PDF receipts | | Support email | Reply-to address on customer-facing emails | | Custom message | Short blurb at the top of receipt emails | | Footer text | Footer line on receipt emails | Each field independently falls through to the merchant-level default when left empty. Setting one field on a project doesn't force you to set the others - leave the unset ones blank and they inherit. ## Setting it up In your [dashboard](https://dashboard.paybridgenp.com), go to **Settings → Custom domain**. The page lists every project on your account. Click **Branding** on the project you want to customize. Each input pre-fills its placeholder with the merchant-level default (so you can see what the project would inherit). Type a value to override; leave blank to keep inheriting. * Logo and Receipt logo: upload an image (PNG, JPG or WebP), or paste an `https://` URL. * Brand color: 6-digit hex (`#1f6feb`). Text on your color stays readable automatically, and the editor shows a live checkout preview as you type. * Support email: standard email format. * Custom message and Footer text: max 280 characters each. Click **Save changes**. The override applies immediately to: * New checkout sessions for this project (logo, theme, and brand color on the hosted page). * New receipt + refund emails for this project. * New PDF receipt downloads. * The buyer activity feed at `id.paybridgenp.com`. Already-issued checkout sessions keep the branding they had at create time. New sessions pick up the override immediately. ## How fall-through works Each field is resolved independently per request. The order is always: 1. **Project value** if set (non-null). 2. **Merchant-level value** otherwise. 3. **Built-in default** if neither is set (e.g. theme falls to `light`). So if your merchant-level brand color is `#1f6feb` and you set project A's brand color to `#3cc850`: * Project A's receipts use `#3cc850`. * Project B (no override set) keeps using `#1f6feb`. * If you later clear project A's brand color, it reverts to inheriting `#1f6feb`. You never lose merchant-level settings by setting a project override. Clearing all project fields takes the project back to inheriting from the merchant. ## What still applies merchant-wide Some settings are operational, not visual, and stay merchant-level: * **Receipt-emails on/off toggle** (`receiptEmailsEnabled`): turn customer receipts on or off across the whole account. There's no per-project disable. * **SMS notification toggles** per template (payment success, payment failed, refund, invoice reminder). * **Tax/VAT settings** (rate, registration number, label). If you want different operational behavior per project (e.g. receipts on for project A, off for project B), tell us at [support@paybridgenp.com](mailto:support@paybridgenp.com) - we'd want to understand the use case before building. ## Where to configure it Branding is configured from the dashboard: open the project and go to **Branding**. Clearing a field reverts it to inheriting your merchant-level default. Public API access for branding isn't available today; if your workflow needs it, tell us at [support@paybridgenp.com](mailto:support@paybridgenp.com). ## See also * [Custom Domain](/guides/custom-domain) - the natural pair: each project gets its own subdomain alongside its own branding. * [Email + Receipt Settings](https://dashboard.paybridgenp.com/settings/emails) - the merchant-level defaults that all projects inherit unless overridden. # Plans and limits Source: https://docs.paybridgenp.com/guides/plans-and-limits What Free, Growth, Pro, and Enterprise include - volume, projects, team seats, SMS receipts, retention, and feature gates. PayBridgeNP has three self-serve plans and a custom Enterprise option. Every plan includes the core Payments API, eSewa, Khalti, Fonepay, hosted checkout, payment links, webhooks, and SDKs. Upgrading raises live-mode limits and unlocks additional features. PayBridgeNP never takes a percentage of your transactions and never holds your funds - payments settle directly to your own eSewa, Khalti, and Fonepay accounts on every plan, including Free. You only pay the flat monthly plan fee plus whatever fee your provider charges. For current prices, see [paybridgenp.com/pricing](https://paybridgenp.com/pricing). This page documents what each plan includes and how the limits are enforced. ## Limits at a glance | | Free | Growth | Pro | Enterprise | | --------------------- | --------------------------- | ----------------------- | ------------------------ | ----------- | | Live volume / month | 50,000 NPR (hard cap) | up to NPR 5 lakh (soft) | up to NPR 20 lakh (soft) | Unlimited | | Projects | 1 | 2 | 5 | Unlimited | | Team members | 1 | 3 | 10 | Custom | | Live SMS receipts | Not included (sandbox only) | 200 / month included | 1,000 / month included | Custom pool | | Webhook endpoints | 1 | 5 | Unlimited | Unlimited | | API keys | 1 | 5 | Unlimited | Unlimited | | Data retention | 30 days | 90 days | Unlimited | Unlimited | | Analytics lookback | 30 days | 90 days | 365 days | 365+ days | | Webhook event history | 14 days | 90 days | 365 days | 365+ days | **Hard cap vs. soft cap:** Free is the only plan that blocks payments once its monthly volume cap is reached - the checkout API returns an error until the calendar month rolls over or you upgrade. Growth and Pro have soft bands instead: crossing 80% or 100% of the band shows a dashboard notice and sends an email, but **payments are never blocked**. Enterprise has no volume limit. **Projects:** the project cap is enforced when you try to create a new project, not retroactively - if you already have more projects than your plan allows, nothing is removed or disabled. **Data retention:** this limits what is **listed**, not what we keep. Nothing is deleted. On a 30-day plan the payments list shows the last 30 days, and older payments stay in your account: move to a longer plan and they are all there again. It applies to the REST API too, not only the dashboard. `GET /v1/payments` and `GET /v1/refunds` return the same window, so a nightly sync into your own books sees 30 days on Free. Fetching a payment by ID is not limited. Three places deliberately ignore the window, because they answer "what happened to my money" rather than "show me recent activity": * **Reconciliation** covers whatever period you pick, on every plan (up to 90 days). * **CSV exports** (payments and refunds) are not retention-limited and return up to the newest 10,000 matching records. * **A single payment** opens by ID at any age, so a link or receipt from last year still works. When an export exceeds 10,000 matching rows, its filename includes `newest-10000`. Otherwise, a selected start date appears as `from-YYYY-MM-DD`. ## What each plan adds The feature gates below describe **live mode**. Most paid developer features are available on every plan in sandbox so you can test before upgrading; [sandbox testing](/guides/sandbox-testing) lists the exceptions. ### Free Everything you need to build and fully test an integration: * Hosted checkout, payment links, webhooks, and the core Payments API * eSewa and Khalti test flows, plus the capped Fonepay sandbox flow described in [sandbox testing](/guides/sandbox-testing) * 1 project, 1 webhook endpoint, 1 API key * 50,000 NPR/month live processing volume * 30-day data retention * Community support (Discord) ### Growth Everything in Free, plus: * Higher live volume band (soft, no blocking) * 2 projects, 3 team members * Customer billing - recurring plans, invoices, dunning reminders (see [Billing](/guides/billing)) * Payment links and [payment buttons](/guides/payment-buttons) for any website * 200 live customer SMS receipts per month included (see [SMS notifications](/guides/sms-notifications)) * 5 webhook endpoints, 5 API keys * 90-day data retention and 90-day analytics/webhook-history lookback ### Pro Everything in Growth, plus: * Higher live volume band (soft, no blocking) * 5 projects, 10 team members * 1,000 live customer SMS receipts per month included * [Fonepay Direct-QR API](/guides/direct-qr) * [Custom checkout domain](/guides/custom-domain) * MCP write scopes (refunds, payment links, subscription changes via an AI agent - see [MCP](/integrations/mcp)) * Audit logs and webhook replay * Unlimited webhook endpoints, API keys, and data retention * 365-day analytics and webhook-history lookback * Priority support ### Enterprise For high-volume businesses or teams running several brands from one account. Enterprise is a custom, sales-assisted plan, not a self-serve signup: * Unlimited live volume, projects, and data retention * A custom team-seat and SMS allowance sized to your business * Bank-app checkout - lets buyers pay from inside their mobile banking app, with no scanning and no separate Fonepay setup * SLA, dedicated support, and migration help for moving off a direct provider integration If you're processing significant monthly volume or think Enterprise might fit, [contact us](https://paybridgenp.com/contact) and we'll put together a plan sized to your business. ## Live SMS receipts Free stays sandbox-only for SMS - every sandbox send is logged but never delivered, on every plan, so you can verify the integration before upgrading. Growth and Pro include a monthly allowance of **live** SMS receipts (200 and 1,000 respectively); sandbox sends never count against it. Once a plan's monthly allowance is used, live sends for the rest of that calendar month are suppressed and logged rather than delivered - **payment processing itself is never affected**, only the customer SMS receipt. The allowance resets at the start of the next calendar month. See [SMS notifications](/guides/sms-notifications) for the full status list and log format. ## Changing plans Upgrades take effect immediately. Downgrades take effect at the end of your current billing period - you keep paid-plan access until then. If a downgrade would put you over the new plan's project or team-seat limit, nothing already in place is removed; the new limit only applies going forward (for example, project creation is blocked once you're at or over the cap, but existing projects keep working). ## Related guides * [Billing](/guides/billing) - recurring plans, invoices, and dunning * [SMS notifications](/guides/sms-notifications) - live SMS allowance and status codes * [Custom domain](/guides/custom-domain) - Pro-plan checkout hostnames * [Direct-QR](/guides/direct-qr) - Pro-plan Fonepay QR API * [Analytics](/guides/analytics) - per-plan lookback windows # Proration Source: https://docs.paybridgenp.com/guides/proration Generate credit/debit invoices when customers change plans mid-period. When a customer upgrades or downgrades their plan in the middle of a billing period, PayBridgeNP can automatically calculate the unused credit on their current plan and the cost of the new plan for the remaining time - then generate a proration invoice for the net difference. ## How proration works Proration is calendar-day based: * **Credit** = (remaining days / total days in period) × current plan amount * **Debit** = (remaining days / total days in period) × new plan amount * **Net** = debit − credit A positive net means the customer owes more (upgrade). A negative net means a credit balance (downgrade) - no invoice is generated in that case. ## Preview before applying Always preview the proration amounts before committing a change so you can show the customer what they'll owe: ```typescript TypeScript theme={null} const preview = await paybridgenp.subscriptions.previewProration( "sub_xxx", "plan_premium_monthly" ); console.log(`Credit: NPR ${preview.creditAmount / 100}`); console.log(`Debit: NPR ${preview.debitAmount / 100}`); console.log(`Net due: NPR ${preview.netAmount / 100}`); ``` ```python Python theme={null} preview = paybridgenp.subscriptions.preview_proration("sub_xxx", "plan_premium_monthly") print(f"Net due: NPR {preview['netAmount'] / 100}") ``` ```php PHP theme={null} $preview = $pb->subscriptions->previewProration("sub_xxx", "plan_premium_monthly"); echo "Net due: NPR " . ($preview['netAmount'] / 100); ``` ## Apply a plan change with proration Pass `prorationBehavior: "create_prorations"` to apply the change immediately: ```typescript TypeScript theme={null} const result = await paybridgenp.subscriptions.changePlan("sub_xxx", { newPlanId: "plan_premium_monthly", prorationBehavior: "create_prorations", }); if ("proration_applied" in result && result.proration_applied) { const invoice = result.proration_invoice; console.log("Proration invoice:", invoice?.id, "Amount:", invoice?.amount_due); } ``` ```python Python theme={null} result = paybridgenp.subscriptions.change_plan("sub_xxx", { "newPlanId": "plan_premium_monthly", "prorationBehavior": "create_prorations", }) if result.get("prorationApplied"): invoice = result.get("prorationInvoice") print("Invoice:", invoice.get("id") if invoice else "No net charge") ``` ```php PHP theme={null} $result = $pb->subscriptions->changePlan("sub_xxx", [ "newPlanId" => "plan_premium_monthly", "prorationBehavior" => "create_prorations", ]); if ($result['prorationApplied']) { echo "Invoice: " . ($result['prorationInvoice']['id'] ?? "no net charge"); } ``` ## Schedule for next billing cycle (default) To keep the existing behavior - plan change applies at next renewal with no proration: ```typescript theme={null} const result = await paybridgenp.subscriptions.changePlan("sub_xxx", { newPlanId: "plan_premium_monthly", // prorationBehavior defaults to "none" }); ``` ## Proration invoice When a proration invoice is created it: * Has `status: "open"` if `netAmount > 0` (customer owes money) * Is skipped (no invoice) if `netAmount ≤ 0` * Contains two line items: `proration_credit` (negative) and `proration_debit` (positive) * Is due immediately - link the customer to the hosted invoice page to collect payment ## API reference | Endpoint | Description | | ---------------------------------------------------------------- | ------------------------------------- | | `GET /v1/billing/subscriptions/:id/preview-proration?newPlanId=` | Preview amounts without committing | | `POST /v1/billing/subscriptions/:id/change-plan` | Change plan; pass `prorationBehavior` | # Provider Fees Source: https://docs.paybridgenp.com/guides/provider-fees How PayBridgeNP tracks and surfaces the fee each payment provider charges, and how to set your negotiated eSewa rate. PayBridgeNP records the provider fee on every successful payment so you can see exactly how much each provider charges, broken down by provider, on your dashboard. Each provider exposes fee information differently - here's how each one works. ## At a glance | Provider | Fee source | Merchant config | | ----------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | **Khalti** | Reported live by Khalti's API on every payment | None - read automatically | | **eSewa** | Computed from your saved rate × payment amount | Set your rate on [Providers](https://dashboard.paybridgenp.com/providers) | | **Fonepay** | Rs 0 on the published merchant-QR schedule; any charges arrive on the settlement report | None - read automatically | ## Khalti Khalti returns the per-transaction fee in its lookup response. We read it on every successful payment and persist it to the payment row. No merchant configuration needed. ## eSewa eSewa does **not** expose per-transaction fees on its API. To track fees for eSewa, you tell PayBridgeNP what rate you have negotiated with eSewa, and we compute the fee at payment time. ### Set your rate 1. Go to **Providers** in the [dashboard](https://dashboard.paybridgenp.com/providers) 2. Find the eSewa card 3. Type your negotiated rate (in percent) into the **Provider fee** input 4. Click **Save** The default is 1.5% - a reasonable public-rate fallback. If your real rate is different, set it before relying on the fee numbers. ### How it's computed For each successful eSewa payment: ``` fee_paisa = round(amount_paisa × fee_bps / 10000) ``` Where `fee_bps` is your saved rate in basis points (150 = 1.50%), saved with your eSewa provider settings. Update it when your agreement changes; fees already recorded stay unchanged. ## Fonepay Fonepay's published price for merchant QR is Rs 0 per transaction, but some payments still carry a charge, such as [instant settlement](https://fonepay.com/content/settlement-options-instant-settlement) or a card-funded QR scan. Fonepay fees appear after its settlement report arrives. Until then, the fee is shown as unknown. Confirm applicable charges with your bank or wallet. ## Where you see it * **Dashboard home** - a "Provider fees" KPI card next to Volume, Payments, and Success rate, plus the period's effective rate. * **Provider breakdown** - fee total and effective rate per provider on the dashboard home. * **Payments list** - a Fee column on every payment row. * **Payment detail** - a Provider fee line, with your eSewa rate shown as a tooltip. * **CSV export** - a `Provider Fee (NPR)` column on the Payments export. ## API access If you consume payment data via the API (or webhooks), the fee surfaces on `payment.metadata` with stable keys: | Key | Type | Set on | | ------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `khalti_fee_paisa` | integer (paisa) | Successful Khalti payments | | `esewa_fee_paisa` | integer (paisa) | Successful eSewa payments | | `esewa_fee_bps` | integer (basis points) | Successful eSewa payments - snapshot of your rate at the time | | `fonepay_fee_paisa` | integer (paisa) | Initially 0; updated when settlement fees are reported. Zero alone does not confirm that the fee was reported. Re-fetch the payment after settlement. | Full and partial refunds do not automatically reduce the recorded provider fee. Check your provider's statement for any fee returned. ## Good to know * Fee totals may be incomplete where fee information is unavailable. * Reporting windows start at midnight UTC, or 05:45 Nepal time. Match the date range and payment timestamps when comparing against a provider statement. # Provider setup Source: https://docs.paybridgenp.com/guides/provider-setup Add your merchant credentials to accept live payments through eSewa, Khalti, and Fonepay. Before you can accept real payments, add your merchant credentials to your project in the [dashboard](https://dashboard.paybridgenp.com). PayBridgeNP uses these to initiate and verify payments on your behalf - money always lands directly in your own provider account. eSewa and Khalti need no credentials for sandbox testing. Built-in test credentials are used automatically. Fonepay is the exception: it has no test environment, so sandbox-mode Fonepay uses your own real merchant credentials and moves real money (capped at NPR 1,000 per payment and NPR 5,000 per month). See [Sandbox Testing](/guides/sandbox-testing). eSewa Wallet checkout with built-in sandbox credentials. Khalti Wallet, bank, and card options through Khalti checkout. Fonepay Dynamic QR paid from Fonepay-connected bank apps. ## What buyers can pay with Each provider gives buyers different ways to pay inside its own checkout. Through **Khalti**, buyers can pay from their Khalti wallet, mobile banking or e-banking of partner banks, or a card. Through **eSewa**, buyers pay from their eSewa wallet. Through **Fonepay**, buyers scan the QR from any Fonepay-network bank app, no separate account needed. ## eSewa ### Get your credentials 1. Log in to your eSewa merchant account at [merchant.esewa.com.np](https://merchant.esewa.com.np) 2. Go to **Profile → Merchant Code** - this is your `merchantCode` 3. Go to **Settings → Payment Integration** to find your `secretKey` ### Add to dashboard 1. Go to **Providers** in the dashboard 2. Click **Configure** next to eSewa 3. Enter your **Merchant Code** and **Secret Key** 4. Click **Save** and toggle the provider on | Field | Description | | -------------- | ------------------------------------------------- | | `merchantCode` | Your eSewa product code | | `secretKey` | HMAC-SHA256 secret from the eSewa merchant portal | *** ## Khalti ### Get your credentials 1. Log in to [admin.khalti.com](https://admin.khalti.com) 2. Go to **API Keys** 3. Copy your **Secret Key** (starts with `live_secret_key_`) Use keys from [admin.khalti.com](https://admin.khalti.com) (live), not [test-admin.khalti.com](https://test-admin.khalti.com) (sandbox). Live keys start with `live_secret_key_`. ### Add to dashboard 1. Go to **Providers** in the dashboard 2. Click **Configure** next to Khalti 3. Enter your **Secret Key** 4. Click **Save** and toggle the provider on *** ## Fonepay Fonepay uses Dynamic QR - customers scan a QR code with their Fonepay-connected banking app to pay. No redirect, no form; the payment is confirmed via WebSocket in real time. ### Get your credentials Fonepay does not issue separate API keys. PayBridgeNP signs in to your Fonepay merchant account with the same username and password you use yourself: 1. Have your Fonepay merchant login ready. 2. Use the **username** and **password** that sign you in. PayBridgeNP detects the account connection and remembers it for your project. PayBridgeNP cannot sign in for you if Fonepay requires a one-time code at every login, or if Fonepay is asking you to set a new password. Complete that step with Fonepay first, then save the new password here. Provider credentials are encrypted at rest and used only for provider requests for your project. Rotate them immediately if you suspect they were exposed. ### Add to dashboard 1. Go to **Providers** in the dashboard 2. Click **Configure** next to Fonepay 3. Enter your **Fonepay Username** (phone number) and **Fonepay Password** 4. Click **Save** and toggle the provider on | Field | Description | | ---------- | ------------------------------------------------------------- | | `username` | Your Fonepay merchant phone number (the one you sign in with) | | `password` | Your Fonepay merchant password | Some Fonepay refunds require a one-time confirmation outside PayBridgeNP. Those refunds are recorded as `requires_action`; follow the provider instructions and mark them complete under **Refunds**. *** ## Verifying your setup After adding credentials: 1. Use your live API key (`sk_live_...`) in your server 2. Create a checkout session for a small amount (NPR 10, the minimum) 3. Complete the payment using your own account on that provider 4. Confirm the payment appears in your provider's merchant dashboard If the payment succeeds end-to-end, your setup is correct. *** ## Security Provider credentials are encrypted at rest and are not returned by the public API. You can replace them at any time from the **Providers** page. *** ## Tracking provider fees Once your providers are configured, PayBridgeNP records the fee each provider charges on every successful payment, broken down per provider on your dashboard. eSewa requires you to set your negotiated rate; Khalti and Fonepay are automatic. See [Provider Fees](/guides/provider-fees) for the details. # Sandbox testing Source: https://docs.paybridgenp.com/guides/sandbox-testing Test eSewa and Khalti safely, and understand Fonepay's real-money exception. Sandbox mode gives eSewa and Khalti test environments for the complete checkout, callback, and webhook flow. Fonepay has no test environment and is the real-money exception explained below. ## How sandbox works When you use a `sk_test_` API key, PayBridgeNP uses built-in test credentials for eSewa and Khalti. You do not configure those providers for sandbox testing. Fonepay requires your own merchant credentials. API keys in sandbox mode start with `sk_test_`. ## Built-in test credentials PayBridgeNP manages sandbox provider credentials platform-side, so you don't enter merchant keys for eSewa or Khalti when using `sk_test_`. Fonepay is different: it has no test environment, so sandbox-mode Fonepay uses your own real Fonepay merchant credentials and moves real money. PayBridgeNP caps it at NPR 1,000 per payment and NPR 5,000 per month to keep test costs small - test with tiny amounts like NPR 10. The **buyer-side** test logins you'll need to complete a sandbox payment are listed under each provider below. ## Running a test payment In the [dashboard](https://dashboard.paybridgenp.com), go to **Settings → API Keys** and copy a key that starts with `sk_test_`. Use this key in your server. Sandbox mode is determined by the key, not a project toggle. ```typescript theme={null} const session = await paybridgenp.checkout.create({ amount: 1000, // NPR 10.00 returnUrl: "https://yoursite.com/success", }); // Open session.checkout_url in your browser ``` On the checkout page, select **eSewa**. You'll be redirected to eSewa's sandbox (`rc-epay.esewa.com.np`). Use these test credentials: * **eSewa ID:** `9806800001` through `9806800005` * **Password:** `Nepal@123` * **MPIN:** `1122` * **Token/OTP:** `123456` On the checkout page, select **Khalti**. You'll be redirected to Khalti's test checkout (`test-pay.khalti.com`). Use these test credentials: * **Khalti ID:** `9800000005` (recommended). Khalti's test wallets are shared by every integrator, and `9800000001` to `9800000004` are often drained or return "insufficient balance", "similar request already being processed", or a 400 error. Those messages come from Khalti's sandbox, not from your integration. * **MPIN:** `1111` * **OTP:** `987654` On the checkout page, select **Fonepay**. A Dynamic QR code is displayed - it refreshes every 3 minutes until paid or the session expires. Scan the QR with any Nepali bank app, exactly as a live buyer would. The payment is confirmed in real time via WebSocket - no page refresh needed. Fonepay has no test environment. Sandbox-mode Fonepay runs against the real Fonepay network with your own merchant credentials (configured in your project's Providers settings), and the money moves for real. PayBridgeNP caps sandbox Fonepay at NPR 1,000 per payment and NPR 5,000 per month - keep test amounts tiny (NPR 10 works fine). After payment, you're redirected back to your `returnUrl`. Check the **Payments** tab in the [dashboard](https://dashboard.paybridgenp.com) - the payment should appear with status **Succeeded**. If you have a webhook configured, check **Webhooks → Delivery Log** to confirm the event was delivered. ## Going live When you're ready to accept real payments: 1. Go to **Settings → API Keys** in the [dashboard](https://dashboard.paybridgenp.com) 2. Copy your live key (starts with `sk_live_`) 3. Go to **Providers** and add your real eSewa, Khalti, and Fonepay credentials (see [Provider Setup](/guides/provider-setup)) 4. Update your server to use the `sk_live_` key That's it. Swapping the key from `sk_test_` to `sk_live_` switches all API calls to live mode. No project toggle required. Live keys process real payments. Make sure your provider credentials are from the production environment - not eSewa's RC environment or Khalti's dev environment. ## Sandbox limitations * Sandbox payments do not appear in your eSewa or Khalti merchant dashboard - they're simulated end-to-end within PayBridgeNP * Fonepay has no test environment - sandbox Fonepay uses your real merchant credentials and moves real money (capped at NPR 1,000 per payment, NPR 5,000 per month; every QR minted in the last 30 minutes counts at full value towards the monthly cap until it is paid or the 30 minutes pass, even if it was replaced or never scanned). Payments appear in your real Fonepay merchant panel * Webhook delivery works normally in sandbox - your webhook URL must be publicly reachable. Use [ngrok](https://ngrok.com) or [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) for local testing # SMS notifications Source: https://docs.paybridgenp.com/guides/sms-notifications Automatic SMS to your customers for payments, refunds, and invoice reminders. Free stays sandbox-only; Growth and Pro include a monthly live allowance. PayBridgeNP sends transactional SMS to your customers' Nepali phone numbers for the events your business cares about - successful payments, failed payments, refunds, and invoice reminders. Every send (and every suppression) is logged so you can verify what went out and why. Free is sandbox-only for SMS - the dispatcher logs each live-mode event as `skipped_free_plan` so you can see what would have gone out, but never contacts the provider. Growth and Pro include a monthly allowance of **live** SMS receipts (200 and 1,000 respectively) - see [Plans and limits](/guides/plans-and-limits). ## What triggers an SMS | Template | Event | Recipient | | -------------------------- | -------------------------------------------------------------------------------------- | --------------------------------- | | `payment.succeeded` | A checkout session completes successfully | The customer who paid | | `payment.failed` | A provider returns failure or the customer abandons | The customer who tried to pay | | `payment.pending_reminder` | A pending payment is nudged with a fresh pay link (e.g. Shopify draft-order reminders) | The customer who has not paid yet | | `refund.processed` | You issue a refund (full or partial) from the dashboard or API | The original customer | | `invoice.created` | A subscription invoice is generated | The billing customer | | `invoice.due` | An open invoice's due date is approaching | The billing customer | | `invoice.overdue` | An invoice passes its due date unpaid | The billing customer | A customer phone number must be present on the checkout session, payment, or billing customer for SMS to fire - there's no separate phone-collection step. ## Sandbox testing - logged, never delivered **In test mode, SMS is never delivered to a real phone.** Every sandbox SMS dispatch is logged in your SMS history with the full message body and a `skipped_sandbox` status - so you can confirm exactly what *would* be sent (template rendering, formatting, the recipient number we resolved) - but nothing reaches the SMS provider or the recipient. A test transaction must never trigger a real message to a real person. This matches the default sandbox email behavior and our [privacy policy](https://paybridgenp.com/privacy) commitment: customer-facing test messages are recorded but not delivered. Billing has one narrow exception for invoice emails addressed to your own PayBridgeNP account email, capped at 50 deliveries per month. To verify the real send path, use your `sk_live_…` key and a real phone number you control. Live sends require a Growth plan or higher, and count against that plan's monthly allowance. ## Live SMS allowance (live mode) In **live mode**, Free accounts never send SMS - the dispatcher resolves merchant entitlements on every send and short-circuits with `status: skipped_free_plan`. Growth and Pro accounts get a monthly allowance of live sends (200 and 1,000 respectively, reset at the start of each calendar month). Once an account has used its allowance for the month, further live sends for that calendar month are suppressed and logged with `status: skipped_live_quota` instead of being delivered. **Payment processing is never affected by the SMS allowance** - only the customer receipt SMS is skipped. The log row still shows the would-have-been-sent body in both cases, handy if you're evaluating whether to upgrade or whether you're approaching your allowance. In **sandbox mode**, no SMS is ever delivered regardless of plan - dispatches are logged only (status `skipped_sandbox`), and sandbox sends never count against your live allowance. When you upgrade, in-flight live events from the moment of upgrade onward send normally against your new plan's allowance. There's no backfill of skipped sends. When you downgrade, the next live event after your billing period ends is evaluated against your new plan's allowance (zero for Free, which produces `skipped_free_plan` again; a lower monthly figure for Growth). No SMS is held in a queue. ## Phone number format The SMS API expects 10-digit Nepali mobile numbers. The dispatcher accepts and normalizes: * `9XXXXXXXXX` (10 digits, starts with 96–99) - used as-is * `977XXXXXXXXXX` (13 digits with country code) - country code stripped * `+977XXXXXXXXXX` (E.164) - formatting and country code stripped Spaces, hyphens, parentheses, and dots are tolerated and stripped. Numbers that don't match these formats log as `skipped_invalid_phone` with the original value preserved for debugging. ## Viewing SMS history The SMS history page in your dashboard (**Account → SMS**) shows every send attempt for the last 30 days, including suppressed ones. Each row carries: * **Status pill** - `sent`, `failed`, `skipped_sandbox`, `skipped_sandbox_quota`, `skipped_free_plan`, `skipped_live_quota`, `skipped_rate_limited`, `skipped_invalid_phone`, `skipped_template_disabled`, or `skipped_no_provider` * **Template** - which event triggered it * **Recipient** - the normalized phone number * **Mode** - `live` or `sandbox` * **Body preview** - the exact text that was (or would have been) sent * **Timestamp** Use the filter pills to narrow by status or mode. The summary cards at the top show running counts for the current month, including how much of your monthly allowance you've used. ## Body templates Bodies are short, plain text, optimized to fit a single SMS segment (160 ASCII characters). They include: * The amount in NPR (e.g. `NPR 1,500`) * Your merchant name as registered in PayBridgeNP * For invoice reminders: a short hosted-token URL the customer can click to pay You cannot customize the body in this release. If you need branded messaging, contact support - we're tracking demand for a v2 with merchant-defined templates. ## Cost SMS receipts are included in your plan up to the monthly allowance (Growth: 200, Pro: 1,000) - there's no separate per-message charge for sends within that allowance. Once you're at the allowance for the calendar month, further live sends are skipped rather than billed; there is currently no way to pay for additional sends beyond the allowance. If your sending volume is consistently at or near the cap, we'll reach out to talk through options. ## Failure modes | Status | What happened | What to do | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `failed` | The provider rejected the message after we attempted to send. Common causes: invalid number not caught by client-side validation, blocked content keywords, provider outage. | Check the `errorMessage` field in the log row. For repeated failures, contact support with the row IDs. | | `skipped_invalid_phone` | The recipient number didn't normalise to a Nepali 10-digit. | Validate phone format on your collection forms. The dispatcher won't retry. | | `skipped_no_provider` | SMS sending wasn't available for this dispatch. You shouldn't see this status in practice. | Contact support if you ever see it. | | `skipped_sandbox` | The session was in test mode. Test-mode SMS is logged but never delivered to a real phone. | Expected in sandbox. Switch to live mode (Growth or higher) to send real SMS. | | `skipped_sandbox_quota` | Legacy status from the former sandbox test quota. Test-mode SMS is now always logged-only, never delivered. | Informational only - no action needed. | | `skipped_free_plan` | Account is on the free plan and the session is in live mode. | Upgrade to Growth or Pro to enable live SMS. Sandbox sends are open to all plans. | | `skipped_live_quota` | The account has used its plan's monthly live SMS allowance. | Wait for the allowance to reset at the start of the next calendar month, or upgrade to a plan with a larger allowance. | ## Privacy and retention The body of each SMS is stored in the log so you can audit what your customers received. Bodies contain only the same information already visible to you in the corresponding payment, refund, or invoice - no additional PII. SMS log rows are retained for the same period as your other transactional data (Free: 30 days, Growth: 90 days, Pro and Enterprise: unlimited). ## Disabling SMS Growth and Pro merchants who want to selectively suppress SMS templates (e.g. send refund SMS but not payment-success SMS) can toggle individual templates in **Settings → Notifications**. Toggling a template off logs subsequent events as `skipped_template_disabled` and does not call the provider. To disable SMS entirely, downgrade to the free plan or contact support to apply a per-merchant override. # Tax (Nepal VAT) Source: https://docs.paybridgenp.com/guides/tax-vat Configure VAT on subscription invoices. 13% by default. Discount-before-tax ordering. PayBridgeNP supports per-merchant tax configuration on subscription invoices. Nepal VAT is 13%. ## Quick start Enable tax and set your PAN: ```bash theme={null} curl -X PATCH https://api.paybridgenp.com/v1/billing/settings/tax \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "enabled": true, "rateBps": 1300, "label": "VAT", "registrationNumber": "301XXXXXXX" }' ``` Rate is stored in basis points: **1300 = 13.00%**. Label defaults to `"VAT"` (rendered as `"VAT (13.00%)"` on invoices). ## Rate changes and past invoices Existing invoices retain the tax rate and registration number used when they were created. The JSON invoice object returned by `GET /v1/billing/invoices/:id` does not currently expose that rate and registration number as separate fields. ## Math - discount before tax PayBridgeNP applies discount **before** tax. Customers pay tax on the discounted price, not the list price: ``` subtotal = 50000 paisa (500 NPR plan) discount_total = 5000 paisa (50 NPR promo) taxable_base = max(0, 50000 - 5000) = 45000 paisa tax_total = round(45000 * 1300 / 10000) = 5850 paisa amount_due = 50000 - 5000 + 5850 = 50850 paisa (508.50 NPR) ``` All integer math internally - amounts stored as paisa (1 NPR = 100 paisa). ## What the customer sees The hosted invoice page and the invoice email show a breakdown: ``` Subtotal NPR 500.00 Discount -NPR 50.00 VAT (13.00%) +NPR 58.50 ───────────────────────────── Total NPR 508.50 ``` When `taxRegistrationNumber` is set, it prints in the invoice footer (e.g. `"PAN: 301XXXXXXX"`). ## Disabling tax Set `enabled: false`. Disabling tax affects future invoices only. # Free trials Source: https://docs.paybridgenp.com/guides/trials Offer a time-based free trial before the first paid invoice. No card on file required. PayBridgeNP supports time-based trials on subscriptions. Trials are configured at the plan level and can be overridden per subscription. ## How trials work 1. Set `trialDays` on a plan (or pass `trialDays` / `trialEndsAt` when creating a subscription). 2. The subscription starts in a `trialing` state - no invoice is generated during the trial window. 3. **72 hours before the trial ends**, PayBridgeNP sends a `subscription.trial_will_end` webhook and emails the customer a heads-up. 4. **When the trial ends**, PayBridgeNP generates the first paid invoice, emails the hosted payment link to the customer, and fires `subscription.trial_ended`. 5. The customer pays the invoice manually - Nepal's redirect-based payment providers (eSewa, Khalti, Fonepay) don't support saved-card auto-charging, so we never silently charge after a trial. ## Setting a trial on a plan ```bash theme={null} curl -X POST https://api.paybridgenp.com/v1/billing/plans \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "name": "Pro", "amount": 99900, "intervalUnit": "month", "trialDays": 14 }' ``` Any subscription created against this plan inherits the 14-day trial. ## Per-subscription override Pass `trialDays` or `trialEndsAt` on `POST /v1/billing/subscriptions` to override the plan default: ```bash theme={null} # 30-day trial regardless of plan setting curl -X POST https://api.paybridgenp.com/v1/billing/subscriptions \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "customerId": "bc_…", "planId": "bp_…", "trialDays": 30 }' # Specific trial end date curl -X POST https://api.paybridgenp.com/v1/billing/subscriptions \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "customerId": "bc_…", "planId": "bp_…", "trialEndsAt": "2026-05-15T00:00:00Z" }' ``` When both are provided, `trialEndsAt` wins. When neither is provided, the plan's `trialDays` is used. `trialDays: 0` disables the trial. ## Ending a trial early Use `POST /v1/billing/subscriptions/{id}/end-trial` to convert the subscription immediately. The first paid invoice is generated synchronously and returned in the response. Guarded - repeating this once the trial is inactive returns `409 trial_not_active`. ## Extending a trial `POST /v1/billing/subscriptions/{id}/extend-trial` with `{ "trialEndsAt": "…" }` pushes the end date forward. Must be strictly after the current trial end, and the trial must still be active. The 72h reminder is re-armed for the new end date. ## Webhooks | Event | When it fires | | ----------------------------- | --------------------------------------------------- | | `subscription.trial_will_end` | \~72h before `trialEndsAt`. Exactly once per trial. | | `subscription.trial_ended` | When the first paid invoice is generated. | | `subscription.trial_extended` | When `extend-trial` is called. | All three carry the full subscription context plus the relevant trial timestamps. See the [webhook verification guide](/guides/webhook-verification) for event payload shapes. ## Disabling the trial feature Want trials switched off for your account? Contact [support@paybridgenp.com](mailto:support@paybridgenp.com) and we'll disable them. Existing trialing subscriptions continue until their `trialEndsAt` elapses. # Webhook Verification Source: https://docs.paybridgenp.com/guides/webhook-verification Verify webhook signatures to ensure events are genuinely from PayBridgeNP. PayBridgeNP signs every webhook delivery with an HMAC-SHA256 signature. You should always verify this signature before processing an event - otherwise anyone could send fake payment notifications to your endpoint. ## Signature format Every webhook request includes an `X-PayBridgeNP-Signature` header: ``` X-PayBridgeNP-Signature: t=1711234567,v1=3b2d4f... ``` | Part | Description | | ---- | ------------------------------------------------------------- | | `t` | Unix timestamp of when the webhook was sent | | `v1` | HMAC-SHA256 of `{timestamp}.{body}` using your signing secret | ## Verifying with the SDK The easiest way is `PayBridgeNP.webhooks.constructEvent()` - it handles signature parsing, HMAC comparison, and replay attack protection (rejects events older than 5 minutes). ```typescript Express theme={null} import express from "express"; import { PayBridgeNP } from "@paybridge-np/sdk"; const app = express(); // Must use raw body - do NOT use express.json() for this route app.post( "/webhooks/paybridgenp", express.raw({ type: "application/json" }), async (req, res) => { const sig = req.headers["x-paybridgenp-signature"] as string; const body = req.body.toString(); let event; try { event = await PayBridgeNP.webhooks.constructEvent( body, sig, process.env.PAYBRIDGENP_WEBHOOK_SECRET!, ); } catch (err) { console.error("Webhook verification failed:", err.message); return res.status(400).send(`Webhook error: ${err.message}`); } switch (event.type) { case "payment.succeeded": await handleSuccess(event.data); break; case "payment.failed": await handleFailure(event.data); break; } res.json({ received: true }); }, ); ``` ```typescript Hono theme={null} import { Hono } from "hono"; import { PayBridgeNP } from "@paybridge-np/sdk"; const app = new Hono(); app.post("/webhooks/paybridgenp", async (c) => { const sig = c.req.header("x-paybridgenp-signature"); const body = await c.req.text(); // raw string let event; try { event = await PayBridgeNP.webhooks.constructEvent( body, sig ?? null, process.env.PAYBRIDGENP_WEBHOOK_SECRET!, ); } catch (err) { return c.json({ error: err.message }, 400); } if (event.type === "payment.succeeded") { await handleSuccess(event.data); } return c.json({ received: true }); }); ``` ```typescript Next.js App Router theme={null} import { NextRequest } from "next/server"; import { PayBridgeNP } from "@paybridge-np/sdk"; export async function POST(req: NextRequest) { const sig = req.headers.get("x-paybridgenp-signature"); const body = await req.text(); // raw string - important let event; try { event = await PayBridgeNP.webhooks.constructEvent( body, sig, process.env.PAYBRIDGENP_WEBHOOK_SECRET!, ); } catch (err) { return Response.json({ error: err.message }, { status: 400 }); } if (event.type === "payment.succeeded") { await handleSuccess(event.data); } return Response.json({ received: true }); } ``` ## Verifying manually (without the SDK) If you're not using the TypeScript SDK: ```python Python theme={null} import hmac import hashlib import time def verify_paybridge_webhook(body: str, signature_header: str, secret: str) -> dict: parts = dict(p.split("=", 1) for p in signature_header.split(",")) timestamp = parts.get("t") v1 = parts.get("v1") if not timestamp or not v1: raise ValueError("Malformed signature header") # Replay attack protection - reject if older than 5 minutes if abs(time.time() - int(timestamp)) > 300: raise ValueError("Timestamp too old") expected = hmac.new( secret.encode(), f"{timestamp}.{body}".encode(), hashlib.sha256, ).hexdigest() if not hmac.compare_digest(expected, v1): raise ValueError("Signature mismatch") import json return json.loads(body) ``` ```php PHP theme={null} function verifyPaybridgeWebhook(string $body, string $signatureHeader, string $secret): array { $parts = []; foreach (explode(',', $signatureHeader) as $part) { [$key, $value] = explode('=', $part, 2); $parts[$key] = $value; } if (empty($parts['t']) || empty($parts['v1'])) { throw new \Exception('Malformed signature header'); } // Replay attack protection if (abs(time() - (int)$parts['t']) > 300) { throw new \Exception('Timestamp too old'); } $expected = hash_hmac('sha256', $parts['t'] . '.' . $body, $secret); if (!hash_equals($expected, $parts['v1'])) { throw new \Exception('Signature mismatch'); } return json_decode($body, true); } ``` ## Important: use the raw request body The HMAC is computed over the **raw** request body string. If you parse the JSON first (e.g. with `express.json()` middleware) and then re-serialize it, the string may differ and signature verification will fail. Always read the body as a string before passing it to `constructEvent`. ## Webhook event payload ```json theme={null} { "id": "evt_2Je91NlWKuXkdXUJOK9gaHNW", "type": "payment.succeeded", "created": 1711234567, "livemode": true, "data": { "id": "pay_6f2jHn2I6F6XLZY8AN698Gax", "amount": 10000, "currency": "NPR", "provider": "khalti", "provider_ref": "KHALTI-TXN-REF", "session_id": "cs_2Je91NlWKuXkdXUJOK9gaHNW", "customer_address": { "line1": "Thamel Road", "line2": null, "city": "Kathmandu", "state": "Bagmati", "postalCode": "44600", "country": "Nepal" } } } ``` `customer_address` is present when the checkout session or payment link had `collectAddress: true`. It is `null` otherwise. `livemode` is `true` when the event was generated by a live (`sk_live_`) key and `false` for sandbox (`sk_test_`). Branch on it in your handler to avoid processing a sandbox event as a real payment, especially when the same handler URL serves both modes. ## Event types | Type | When | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `payment.succeeded` | Payment was verified successfully | | `payment.failed` | Payment attempt failed or was declined | | `payment.cancelled` | The customer backed out on the provider page. The checkout session stays open for retry, so no `id` (payment id) is present - the payload carries `session_id`, `provider`, `amount`, `currency`, `metadata`. Can fire more than once per session if the customer abandons several attempts | | `payment.refunded` | A payment was refunded, partially or fully - compare `refunded_amount` with `total_refunded` in the payload | | `payment_link.paid` | A customer paid through a payment link | Provider-confirmed duplicate captures are shown in the dashboard/admin tools for refund handling, but they do not emit `payment.succeeded` or `payment_link.paid` for the superseded checkout session. This list is not exhaustive - billing, subscription, coupon, and dunning events fire too. See the [billing](/guides/billing), [coupons](/guides/coupons), and [dunning](/guides/dunning) guides for those event types. ## Retry schedule If your endpoint returns a non-2xx status or times out, PayBridgeNP retries with exponential backoff: | Attempt | Delay after previous | | ------- | -------------------- | | 1 | 30 seconds | | 2 | 5 minutes | | 3 | 30 minutes | | 4 | 2 hours | | 5 | 8 hours | After 5 failed attempts the delivery is marked as permanently failed. You can see all delivery attempts and their response codes in the **Webhooks → delivery log** in your dashboard. ## Testing webhooks locally Your webhook endpoint must be publicly reachable. For local development, use a tunnel: ```bash theme={null} # ngrok ngrok http 3000 # Cloudflare Tunnel (free, no account required for temporary tunnels) cloudflared tunnel --url http://localhost:3000 ``` Use the public URL as your webhook endpoint in the dashboard during development. ## Inspecting payloads in the browser Want to sanity-check your HMAC implementation without running any server code? Use the [Webhook Debugger](https://paybridgenp.com/tools/webhook-debugger) - paste your signing secret, the raw request body, and the `X-PayBridgeNP-Signature` header, and it tells you whether the signature would verify and why. # Accept eSewa, Khalti, and Fonepay with one API Source: https://docs.paybridgenp.com/home PayBridgeNP developer documentation. Accept eSewa, Khalti, and Fonepay payments in Nepal through one API, with sandbox testing, signed webhooks, SDKs, and store plugins.

Developer documentation

Accept eSewa, Khalti, and Fonepay with one API.

One checkout call, one signed webhook, and money that settles to your own merchant accounts. Hosted checkout, payment links, subscriptions, Direct QR, and a native mobile sheet.

  • Sandbox on every plan
  • Signed webhooks
  • 0% per transaction

payment.succeeded

NPR 4,500

eSewa·Verified·Signed

Buyer pays

PayBridgeNP verifies

Your server gets the webhook

Inside the Mobile SDK

One sheet, every method.

Present eSewa, Khalti, Fonepay QR, and bank apps without rebuilding checkout for each provider.

Explore the Mobile SDK →

Ask your AI

Talk to your payments.

Connect Claude or ChatGPT to your PayBridgeNP account, or script it from the CLI.

Two ways in

CLInpm install -g @paybridge-np/cliMCP

PayBridgeNP Docs

Everything you need to accept payments in Nepal with one API and your own provider accounts.

# Connect Claude, ChatGPT, or Cursor to PayBridgeNP Source: https://docs.paybridgenp.com/integrations/mcp Set up the PayBridgeNP MCP server, the hosted connector for Claude and ChatGPT or the local npm package for Cursor, Claude Code, and VS Code, with the scope, security, and tool reference. This is the setup and reference page. For what a merchant can do with an assistant once it is connected, see [Ask Claude or ChatGPT about your eSewa, Khalti, and Fonepay payments](https://paybridgenp.com/mcp). The official Model Context Protocol (MCP) server for PayBridgeNP. Lets any MCP-compatible AI assistant - Claude on web and mobile, Claude Desktop, ChatGPT, Cursor, Claude Code, VS Code, Continue - talk to your PayBridgeNP account through 70 typed tools. There are two ways to connect, with the same tools either way: * **Hosted (remote)** - add PayBridgeNP as a connector in Claude (web, mobile, desktop) or ChatGPT, sign in, and approve access on a consent screen. Nothing to install. This is the only option on mobile. [Jump to the hosted setup](#connect-a-hosted-app-claude-web-mobile-chatgpt). * **Local (npm)** - run the server on your own machine with a scoped token you generate. Good for Cursor, Claude Code, and VS Code. [Jump to the local setup](#run-the-local-server-cursor-claude-code-vs-code). Either way the model only ever sees JSON results, never your raw API key, and every tool call goes over HTTPS to `api.paybridgenp.com` and runs through the same permission checks as our REST API. Source code is open at [github.com/paybridgenp/paybridgenp-mcp](https://github.com/paybridgenp/paybridgenp-mcp). Audit exactly what runs against your key before installing. ## Connect a hosted app (Claude web, mobile, ChatGPT) No install, no token to copy. The app sends you to PayBridgeNP to sign in and approve. 1. In your assistant's connector settings, add a custom connector with the URL `https://api.paybridgenp.com/mcp`. * **Claude**: Settings → Connectors → Add custom connector. * **ChatGPT**: Settings → Connectors → Add (developer mode). 2. The app opens a PayBridgeNP sign-in page. Log in with your merchant account. 3. On the consent screen, choose the **project**, the **mode** (sandbox or live), and tick exactly the **access** you want to grant - read-only by default, with write access opt-in per category. Live-mode write access requires a Pro plan; sandbox write access is available on every plan. 4. Approve. You are sent back to the app, connected. You can revoke a connected app any time from [`dashboard.paybridgenp.com/mcp`](https://dashboard.paybridgenp.com/mcp) - it stops working immediately. Actions that ask for confirmation (like refunds) are not available over the hosted connector yet, because the confirmation step needs a channel the remote protocol does not have. Use the local server for those, or wait for the confirm-in-chat fallback. Everything else - reads, payment links, billing objects - works. ## Run the local server (Cursor, Claude Code, VS Code) ### 1. Generate a scoped token Open [`dashboard.paybridgenp.com/mcp`](https://dashboard.paybridgenp.com/mcp). Click **Generate token**. The new-token form lets you pick: * **Description** - a label so you can find this token later in the audit log (e.g., "Claude Desktop on my laptop"). * **Scopes** - grouped Read / Write / Sensitive. Read-only is the safe default; opt in to writes per category. * **Expires** - 7d, 30d, 90d, or never. 30d is recommended. * **Spend cap (paisa, 24h rolling)** - auto-prefilled to Rs 50,000 when any write scope is enabled. Hard cap on the rolling 24-hour sum of refunds + checkout sessions originated by this token. Hit **Generate**. The raw `sk_test_…` or `sk_live_…` token is shown **once** - copy it immediately. We can't show it again. ### 2. Paste into your AI assistant #### Claude Desktop Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows): ```json theme={null} { "mcpServers": { "paybridge": { "command": "npx", "args": ["-y", "@paybridge-np/mcp@latest"], "env": { "PAYBRIDGENP_API_KEY": "sk_live_..." } } } } ``` Restart Claude Desktop. #### Cursor Same JSON in `.cursor/mcp.json` (or via Settings → MCP). ```json theme={null} { "mcpServers": { "paybridge": { "command": "npx", "args": ["-y", "@paybridge-np/mcp@latest"], "env": { "PAYBRIDGENP_API_KEY": "sk_live_..." } } } } ``` #### Claude Code ```bash theme={null} claude mcp add paybridge \ --env PAYBRIDGENP_API_KEY=sk_live_... \ -- npx -y @paybridge-np/mcp@latest ``` #### VS Code `.vscode/mcp.json`: ```json theme={null} { "servers": { "paybridge": { "type": "stdio", "command": "npx", "args": ["-y", "@paybridge-np/mcp@latest"], "env": { "PAYBRIDGENP_API_KEY": "sk_live_..." } } } } ``` #### Claude web, mobile, or ChatGPT These use the hosted connector instead of a local token - see [Connect a hosted app](#connect-a-hosted-app-claude-web-mobile-chatgpt) above. ### 3. Try it Open your assistant and type something natural: > Refund the last successful payment from `customer@example.com`. The agent picks the right tool calls (`list_payments` → `get_payment` → `create_refund`), and PayBridgeNP's MCP server pops up a confirmation modal asking you to approve the refund before it executes. You see the refund result inline. ## What you can ask The agent figures out the right tool calls. You don't write them by hand. * *"Show me payments above Rs 5,000 from this week, grouped by provider."* * *"Why did the webhook to checkout.acme.com fail yesterday at 3pm?"* * *"Create a payment link for Rs 2,500 titled 'Course access - December batch'."* * *"What's my analytics overview for the last 30 days - gross volume, success rate, refunds?"* * *"List all subscriptions that are past due, with the customer email."* * *"Pause the subscription for [ram@sherpa.com](mailto:ram@sherpa.com) until the end of the month."* ## Tools 70 tools across read + write. Read tools cover the entire account surface; write tools cover refunds, checkout creation, payment-link CRUD, webhook CRUD, and the full billing surface - plans, customers, subscriptions, invoices, coupons, promotions, tax, dunning, and usage metering. ### Read (33 tools) | Tool | Scope | What it does | | ------------------------------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------- | | `get_account` | `account:read` | Merchant + project + key context (PII masked unless `pii:read`) | | `search_docs` | `docs:read` | Search these docs (guides, API reference, providers) and return matching pages with a snippet | | `list_payments` / `get_payment` | `payments:read` | Inspect payments with full provider + customer detail | | `list_refunds` / `get_refund` | `refunds:read` | Refund history, status, failure reason | | `list_checkout_sessions` / `get_checkout_session` | `sessions:read` | In-flight + abandoned checkouts | | `list_payment_links` / `get_payment_link` | `links:read` | Links + view/conversion stats inline | | `list_webhook_endpoints` / `list_webhook_deliveries` | `webhooks:read` | Endpoint config + per-delivery debug | | `list_plans` / `get_plan` | `billing:read` | Subscription billing plans | | `list_customers` / `get_customer` | `billing:read` | Billing customers | | `list_subscriptions` / `get_subscription` | `billing:read` | Active + past-due subscriptions | | `list_invoices` / `get_invoice` | `billing:read` | Subscription invoices | | `list_invoice_items` | `billing:read` | Pending one-off invoice items on a subscription | | `list_coupons` / `get_coupon` | `billing:read` | Discount coupons and their redemption stats | | `list_promotion_codes` / `get_promotion_code` / `validate_promotion_code` | `billing:read` | Promotion codes - list, inspect, or validate a code before applying | | `get_tax_settings` | `billing:read` | Active tax configuration for the account | | `list_dunning_policies` / `get_dunning_invoice_status` | `billing:read` | Dunning policy definitions + per-invoice dunning state | | `preview_subscription_proration` | `billing:read` | Estimate the prorated credit/charge before a plan change | | `get_subscription_usage_summary` / `list_usage_records` | `billing:read` | Metered usage totals and raw usage record history | | `get_analytics_overview` | `analytics:read` | Headline KPIs (total / success rate / volume / funnel / per-provider) over a 1–90 day window | ### Write (37 tools) Every write tool is annotated `destructiveHint`. Money-moving tools always elicit confirmation in the host UI before executing. | Tool | Scope | Confirms? | | --------------------------------------------------------------- | ---------------- | ------------------------------------ | | `create_checkout_session` | `payments:write` | If `amount > 5,000` (paisa: 500,000) | | `create_refund` | `refunds:write` | **Always** | | `create_payment_link` | `links:write` | No | | `update_payment_link` | `links:write` | No | | `cancel_payment_link` | `links:write` | No (soft-deactivate, reversible) | | `delete_payment_link` | `links:write` | **Always** (only if never used) | | `create_webhook_endpoint` | `webhooks:write` | No (idempotent create) | | `update_webhook_endpoint` | `webhooks:write` | No | | `delete_webhook_endpoint` | `webhooks:write` | **Always** | | `create_plan` / `update_plan` | `billing:write` | No | | `create_customer` / `update_customer` | `billing:write` | No | | `add_customer_credit` | `billing:write` | No | | `create_subscription` | `billing:write` | No | | `pause_subscription` / `resume_subscription` | `billing:write` | No | | `cancel_subscription` | `billing:write` | **Always** | | `change_subscription_plan` | `billing:write` | No | | `end_subscription_trial` / `extend_subscription_trial` | `billing:write` | No | | `create_invoice_item` / `delete_invoice_item` | `billing:write` | No | | `create_coupon` / `deactivate_coupon` | `billing:write` | No | | `create_promotion_code` / `deactivate_promotion_code` | `billing:write` | No | | `apply_coupon_to_subscription` / `remove_subscription_discount` | `billing:write` | No | | `update_tax_settings` | `billing:write` | No | | `create_dunning_policy` / `update_dunning_policy` | `billing:write` | No | | `set_subscription_dunning_policy` | `billing:write` | No | | `stop_invoice_dunning` | `billing:write` | **Always** | | `retry_invoice_dunning_now` | `billing:write` | No | | `report_subscription_usage` / `update_subscription_quantity` | `billing:write` | No | ## Prompts 6 built-in prompt templates that appear as slash commands and menu items in Claude Desktop and Cursor. Select one, fill in any arguments, and the agent picks up from there - no need to remember tool names or craft instructions manually. | Prompt | What it does | Arguments | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | | `daily_summary` | End-of-day digest - revenue, payment counts, refunds, checkout funnel completion rate, and any failed webhook deliveries | None | | `monthly_reconciliation` | Full month reconciliation table: gross volume, net after refunds, success rate, provider breakdown, top 5 customers. Ready to share with an accountant | `month` (optional, e.g. `2026-04`) | | `investigate_failed_payment` | Diagnoses why a payment or batch of payments failed. Checks provider error codes, checkout session funnel, and recommends retry / contact customer / escalate | `payment_id` (optional) or `customer_email` (optional) | | `onboard_customer` | Creates a billing customer and subscribes them to a plan in one flow. Validates the plan, optionally validates and applies a promotion code, then confirms subscription details | `email`, `name`, `plan_id` (required); `coupon_code` (optional) | | `review_dunning` | Lists all past-due invoices, shows retry count and next scheduled retry for each, and categorizes them as Retry now / Stop dunning / Wait. Asks for confirmation before acting | None | | `apply_discount` | Validates a promotion code, shows the before/after price, then applies the discount to a subscription after confirmation | `subscription_id`, `promotion_code` | Prompts are available in any host that supports MCP prompts - Claude Desktop ≥ 0.7, Cursor ≥ 0.42, Claude Code current. In Claude Desktop, open the prompt picker with `/` and type `paybridge` to filter to PayBridgeNP prompts. ## Scope reference 16 scopes. Tokens hold an explicit allowlist - anything not in the list returns `403 insufficient_scope` at the API gateway, never reaching the handler. ### Read scopes (granted by default) | Scope | Unlocks | | ---------------- | --------------------------------------------------- | | `account:read` | `get_account` | | `payments:read` | `list_payments`, `get_payment` | | `refunds:read` | `list_refunds`, `get_refund` | | `sessions:read` | `list_checkout_sessions`, `get_checkout_session` | | `links:read` | `list_payment_links`, `get_payment_link` | | `webhooks:read` | `list_webhook_endpoints`, `list_webhook_deliveries` | | `billing:read` | All `list_*` / `get_*` billing tools | | `analytics:read` | `get_analytics_overview` | | `audit:read` | (reserved - `list_audit_logs` shipping next) | | `docs:read` | `search_docs` | ### Write scopes (opt in per scope) | Scope | Unlocks | | ---------------- | ------------------------------------------------------------------------------------------ | | `payments:write` | `create_checkout_session` | | `refunds:write` | `create_refund` | | `links:write` | `create_payment_link`, `update_payment_link`, `cancel_payment_link`, `delete_payment_link` | | `webhooks:write` | webhook CRUD | | `billing:write` | All billing CRUD + lifecycle | ### Sensitive | Scope | Effect | | ---------- | ---------------------------------------------------------------------------------------------- | | `pii:read` | Returns raw `customer_email` and `customer_phone` instead of masked. Never granted by default. | ## Security model Scopes, confirmation prompts, spend caps, and rate limits constrain what each token can do. ### 1. Scope enforcement Scopes are enforced on every API route before your request reaches the handler. Tokens created for MCP carry an explicit scope allowlist; classic REST API keys are unscoped and keep full access for backward compatibility. ### 2. Elicitation gates on destructive actions Money-moving tool calls send an MCP `elicitation/create` request to the host. The host shows a confirmation modal with the proposed amount, customer, and action. The tool only proceeds on `accept`. Hosts that don't support elicitation - usually older clients - fail closed with `elicitation_unsupported` and an actionable error pointing back to the dashboard. For `create_refund`, the server hydrates the underlying payment first, so the modal shows the real amount + customer + provider, not just an opaque payment id. ### 3. 24-hour rolling spend cap Every checkout session and refund records its `originating_api_key_id`. Before each `POST /v1/checkout` and `POST /v1/refunds`, the server sums the originating spend in the trailing 24 hours; if `current + new > spend_cap_paisa`, the call is rejected with HTTP 403: ```json theme={null} { "error": { "message": "MCP token spend cap exceeded. Last 24h spend: Rs 400.00, cap: Rs 500.00, remaining: Rs 100.00.", "type": "permission_error", "code": "over_spend_cap", "spent_paisa": 40000, "cap_paisa": 50000, "remaining_paisa": 10000, "reset_at": "2026-04-19T00:21:12.556Z", "request_id": "req_..." } } ``` ### 4. Per-token rate limits Three buckets, all per-token: * **Total**: 60 calls / 60s on the payment routes (payments, refunds, checkout, sessions, links, webhooks, analytics, QR, account); billing routes use the project-level limit of 120 a minute instead * **Destructive**: 6 calls / 60s * **Elicitation-gated**: 2 calls / 60s Returns `429` with `Retry-After` and `X-RateLimit-Bucket` headers. REST keys are unaffected for back-compat. ### Other defenses * **Audit trail.** Tool activity records the token, assistant, action, and time in your dashboard. * **No token passthrough.** The MCP server never forwards your token to upstream provider APIs. Khalti / eSewa secrets stay server-side. * **Untrusted-data handling.** Customer-controlled text is treated as data rather than instructions, and credential-shaped values are redacted before they reach the assistant. * **PII masking.** `customer_email` shows as `ra***@example.com`; `customer_phone` shows as `***1234`. Unmask only by granting `pii:read`. * **One-click revoke.** Find the token in the dashboard and click the trash icon. The next call returns 401 within seconds. ## Audit + activity feed Open [`dashboard.paybridgenp.com/mcp`](https://dashboard.paybridgenp.com/mcp). The **Recent activity** card shows the last 24h of MCP-originated events: * `mcp_token.created` / `mcp_token.revoked` * `refund.created` (with `via: "mcp"`, `mcp_client_name`, `api_key_id`) * `checkout_session.created` * `webhook_endpoint.created` / `.updated` / `.deleted` Pro-plan merchants see the full audit history at `/logs` with the same MCP tagging. ## Configuration reference The MCP server reads two env vars when you run it: | Env var | Required | Default | | -------------------------- | -------- | ----------------------------- | | `PAYBRIDGENP_API_KEY` | yes | - | | `PAYBRIDGENP_API_BASE_URL` | no | `https://api.paybridgenp.com` | You can also pass `--api-key=sk_live_...` as a CLI argument instead of the env var. ## Troubleshooting ### Tools don't show up in my assistant 1. Restart the assistant after editing config - most hosts only re-read MCP configs at startup. 2. Run `npx -y @paybridge-np/mcp@latest` in a terminal manually. If it errors, the message will tell you what's wrong (usually a missing or revoked token). 3. Check the host's MCP log. Claude Desktop: `~/Library/Logs/Claude/mcp*.log`. ### Every tool call returns 401 Token is revoked, expired, or the env var didn't survive into the host's environment. Generate a fresh token from the dashboard and replace. ### Every write call returns `insufficient_scope` Your token is read-only. Generate a new token with the relevant write scopes ticked. Don't reuse the read-only token - issue a separate write-scoped token with a tight spend cap so you can revoke it independently. ### `over_spend_cap` after running a few refunds Either raise the cap on the token or wait for the 24h window to roll. The error response includes `reset_at` - that's when the oldest counted activity ages out. ### `elicitation_unsupported` on a destructive tool Your host doesn't implement MCP elicitation. Update the host (Claude Desktop ≥ 0.7, Cursor ≥ 0.42, Claude Code current), or perform the action in the PayBridgeNP dashboard instead. ### Customer names show up wrapped in `` That's deliberate - it tells the model not to interpret merchant-controlled free text as instructions. Defense against prompt injection in metadata / customer fields. Don't strip the wrappers. ### How do I see what the agent is actually doing? The dashboard's **Recent activity** card shows every MCP tool invocation with the assistant's name, the action, and timestamps. Hosts also typically log tool calls - Claude Desktop shows them inline; Cursor logs them to its dev console. ## Roadmap * **Confirm-in-chat for write actions** over the hosted connector, so refunds and other confirmation-gated tools work without the local server. * **More audit + search tools** (`list_audit_logs`, `search_paybridge`). * **AP2 mandate hooks** for agent-initiated commerce (forward-compat). Track progress at [github.com/paybridgenp/paybridgenp-mcp](https://github.com/paybridgenp/paybridgenp-mcp). # How the Shopify Integration Works Source: https://docs.paybridgenp.com/integrations/shopify/how-it-works End-to-end payment, refund, and draft-order flows for PayBridgeNP on Shopify. This page walks through every flow the Shopify app supports - what the customer sees, what the merchant sees, and how the order ends up marked Paid in Shopify. Read it once after installation and you'll know where to look when something doesn't behave as expected. ## Main payment flow From the moment a customer clicks **Complete order** to the moment their Shopify order shows **Paid**: The customer picks **Pay with eSewa, Khalti, or Fonepay** as their payment method on your Shopify checkout page and clicks **Complete order**. Shopify creates the order in `pending` state and lands the customer on the native Shopify thank-you page. The app receives the `orders/create` webhook, confirms the payment method matches your configured manual method, and mints a PayBridgeNP checkout session for the order amount with the customer's contact details. PayBridgeNP queues an **SMS** and an **email** to the customer with the payment link. Delivery is usually under 30 seconds. Each payment link is valid for **30 minutes**; because most reminders fire after that window, every reminder mints a **fresh link**, so an expired link is never a problem. The customer can also see a **Pay now** button inside their Shopify customer account order page (if you've added the optional Customer Account block - see [Install](/integrations/shopify/install)); that button always generates a working link on demand. The customer taps the SMS / email link (or the Pay now button), lands on the PayBridgeNP hosted checkout page, picks a wallet (eSewa / Khalti / Fonepay), and completes the payment. Once the wallet confirms the payment, the app marks the Shopify order as **Paid** via the Shopify Admin API. Typical end-to-end latency from wallet confirmation to the order flipping: under 5 seconds. ### Where you see it in the admin Every PayBridgeNP order shows up in the embedded **Orders** page inside Shopify admin, filterable by payment status: The embedded Orders page showing Shopify orders with their PayBridgeNP status (Awaiting, Paid, Refunded), filter tabs by status, reminder counts, and per-row Send reminder action buttons Each row shows the order number, current status, amount, customer, when it was created, how many reminders have been sent, and a per-row **Send reminder** action for awaiting payments. Tabs at the top filter by payment state: All, Awaiting, Paid, Failed, Expired, Cancelled. ### What if the customer leaves without paying? The app sends branded SMS and email reminders on a fixed schedule - 30 minutes, 4 hours, and 24 hours after the order - and if the customer still hasn't paid, the order is automatically cancelled and inventory is released 48 hours after it was placed. The reminder schedule and auto-cancel window are fixed; the app settings only let you turn the SMS and email reminders on or off. ## Refund flows PayBridgeNP is a [manual payment method](https://help.shopify.com/en/manual/payments/shopify-payments/refunds) from Shopify's point of view. Shopify can't refund a manual payment method through its built-in admin UI - the money has to come out of PayBridgeNP's wallet integration, not Shopify's. The app supports two ways to initiate a refund: ### Flow A - Merchant clicks Refund in Shopify admin (redirected to the dashboard) Clicking **Refund** in Shopify admin does **not** refund the order in-admin - Shopify can't refund a manual payment method. The app catches the attempt and points the merchant to the PayBridgeNP dashboard, where the actual refund happens: 1. Merchant opens the order in Shopify admin and clicks **Refund** 2. The refund is **not** processed in Shopify. The app catches the attempt and surfaces a banner on the embedded **Orders** page telling the merchant to complete the refund from the PayBridgeNP dashboard 3. Merchant opens the [PayBridgeNP dashboard](https://dashboard.paybridgenp.com), finds the payment, clicks **Refund** 4. PayBridgeNP processes the refund with the wallet provider 5. The app mirrors the refund onto the Shopify order automatically (see Flow B below) ### Flow B - Merchant clicks Refund in the PayBridgeNP dashboard This is the path the app is really built for. Skip Flow A's first two steps and go straight to the PayBridgeNP dashboard. 1. Merchant finds the payment in [PayBridgeNP dashboard](https://dashboard.paybridgenp.com) and clicks **Refund**, entering the amount 2. PayBridgeNP processes the refund with the wallet 3. The Shopify order automatically flips to **Partially refunded** or **Refunded** depending on the cumulative amount ### Full vs partial refunds | Cumulative refund amount | Result on the Shopify order | | ------------------------ | ------------------------------------------------------------------- | | Equal to order total | Order flips to **Refunded** | | Less than order total | Order flips to **Partially refunded**; line items are NOT restocked | ## Draft order payment links Beyond the main checkout flow, the app supports sending a PayBridgeNP payment link for an existing Shopify draft order. This is useful for over-the-phone orders, B2B invoices, or any case where the buyer isn't placing the order themselves. 1. In Shopify admin, build a draft order the usual way: pick a customer (must have email or phone), add products, set a price, save 2. In the embedded PayBridgeNP app, go to **Draft orders** 3. Find the draft, click **Send payment link** 4. The customer receives an SMS + email with the PayBridgeNP checkout URL. Once they pay, the order is marked Paid just like the main flow. The Draft orders page in the embedded PayBridgeNP app showing an open Shopify draft order with customer details and a Send payment link action button The merchant never has to touch the payment link themselves or copy-paste URLs. The draft's customer is the recipient. Enable this flow in the **Draft-order send-link** section of the Settings page. Live mode requires a PayBridgeNP Growth plan or higher; sandbox is available on every plan. ## What happens on uninstall When a merchant uninstalls the app from Shopify admin, the app stops accepting webhooks for that shop and stops sending reminders for any orders still awaiting payment. The shop's stored credentials are cleared. If you later reinstall, paste your PayBridgeNP API key again from the embedded settings page and you're back in business. ## If your API key stops working A revoked, expired, or paused PayBridgeNP key used to fail silently: Shopify would still show the order, but the app could not create a payment for it and nothing recorded that it had tried. Now the app keeps every order it had to refuse for a key problem, and shows you: * a red notice on the app's Home and Settings pages, with the reason (for example, key revoked). The **Connected** badge only turns green when the key actually works; * a **Needs attention** list on the Orders page with each refused order and a **Replay** button. Paste a working key on Settings, then press **Replay** on each order. The app re-reads the order from Shopify, checks it is still unpaid, and creates the payment link exactly as it would have on the day the order was placed. Orders refused before this was added (September 2026) were not recorded and have to be handled with a manual payment link. ## Need more detail? * [Install the app](/integrations/shopify/install) - step-by-step setup * [Troubleshooting](/integrations/shopify/troubleshooting) - common issues and how to fix them # Install PayBridgeNP on Shopify Source: https://docs.paybridgenp.com/integrations/shopify/install Step-by-step setup - install the app, create the manual payment method, paste your API key, place a test order. The self-serve setup takes about 30 minutes after we provision your private app. Requesting and provisioning the install link takes up to one business day. No code changes on your site. ## Before you start You need: 1. A **Shopify store** - any plan, dev store or live store, as long as its currency is **NPR** 2. A **PayBridgeNP API key** - get one from the [PayBridgeNP dashboard](https://dashboard.paybridgenp.com) under **Settings → API Keys**. Use `sk_test_…` to test, `sk_live_…` when you're ready to take real payments ## Step 1 - Get your install link and install the app Each store gets its own private PayBridgeNP app, set up for you by the team. 1. Fill in the form at [paybridgenp.com/integrations/shopify](https://paybridgenp.com/integrations/shopify#install) with your shop domain (`yourstore.myshopify.com`) and the email your install link should go to. We send the link within one business day, usually the same day. You can also email [support@paybridgenp.com](mailto:support@paybridgenp.com?subject=Shopify%20install%20request) instead if you prefer. 2. Open the link. You'll see Shopify's standard install consent screen, which lists the permissions the app needs - reading and updating orders, creating draft orders, reading and updating customer info (to send the payment-link SMS and email and attach the payer to the order), and reading product info. 3. Click **Install app**. Shopify redirects you to the embedded PayBridgeNP settings page inside your admin. The install link is tied to your exact `.myshopify.com` domain and only works for that store. Running more than one store? Submit the form once per store. ## Step 2 - Create a Shopify manual payment method This is the step most merchants miss. **Until you do this, no order will ever flow through PayBridgeNP.** 1. In your Shopify admin, go to **Settings → Payments** 2. Scroll to **Manual payment methods** (under "Additional payment methods") 3. Click **Add manual payment method** 4. At the bottom of the dropdown, pick **Create custom payment method** 5. Fill in the form: | Field | Value | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | **Custom payment method name** | `Pay with eSewa, Khalti, or Fonepay` (or any name you prefer - you'll paste the exact same string into the app settings in Step 4) | | **Additional details** | `You'll receive a payment link by SMS and email after placing your order.` | | **Payment instructions** | `After placing your order, you'll receive a secure payment link via SMS and email to complete payment with eSewa, Khalti, or Fonepay.` | 6. Click **Activate** Your customers will now see this payment method at checkout. The **Custom payment method name** you enter here must match exactly what you paste into the app's **Payment method name** field in Step 4. The match is case-insensitive, but any typo or extra whitespace will cause orders to be silently ignored. ## Step 3 - Paste your PayBridgeNP API key Back in the embedded app (still in your Shopify admin, under **Apps → PayBridgeNP → Settings**): The Settings page showing a successful PayBridgeNP connection with a masked API key, a Test mode toggle, and instructions for creating a custom payment method in Shopify admin 1. Find the **PayBridgeNP secret key** field 2. Paste your `sk_test_…` or `sk_live_…` key 3. Click **Save** The app validates the key against your PayBridgeNP account and registers a webhook on PayBridgeNP for automatic order reconciliation. **Test mode** is on by default - toggle it off when you're ready to take real payments with an `sk_live_` key. If the key is invalid, you'll see a red banner with the specific error from PayBridgeNP. Common causes when the key is rejected: * Wrong environment (you pasted a live key but your PayBridgeNP project is in sandbox, or vice versa) * Key was revoked or rotated in the PayBridgeNP dashboard ## Step 4 - Paste the payment method name On the same settings page, find the **Payment method name (must match exactly)** field and paste **exactly the same string** you used for the Shopify payment method name in Step 2 (e.g. `Pay with eSewa, Khalti, or Fonepay`). Click **Save**. This tells the app which Shopify payment method to intercept. When an order comes in with a different payment method (cash on delivery, bank transfer, etc.), it's left alone. ### Other settings on the same page * **Customer notifications** - SMS and email reminders are on by default. They fire when a customer places an order but doesn't pay within the configured window. * **Draft-order send-link** - off by default. Enables a Draft orders page inside the app where you can pick a Shopify draft order and send the customer a PayBridgeNP payment link. Live mode requires Growth or higher; sandbox is available on every plan. ## Step 5 - Optional: Add the "Pay now" block to the Customer Account order page This is optional but improves the experience for buyers who close the tab without paying. 1. In Shopify admin, open **Settings → Customer accounts** 2. Click **Customize** on your Customer Accounts theme 3. Navigate to the **Order status** page in the editor's left nav 4. Click **Add block** and pick **PayBridgeNP Pay Now (customer account)** 5. Click **Save** Now when a returning buyer opens their order page in their Shopify customer account, they'll see a **Pay now** button that takes them directly to the PayBridgeNP checkout. They no longer need the SMS or email link. ## Step 6 - Place a test order Go to your storefront, add any product to cart, proceed to checkout, and at the payment step pick your PayBridgeNP payment method. Click **Complete order**. ### What should happen 1. Shopify creates the order in **Pending** state and lands the customer on Shopify's native thank-you page 2. Within \~30 seconds, the customer receives an **email** from "Your Store via PayBridgeNP" (their sender name is your store's name) containing a "Pay securely" button linking to the PayBridgeNP hosted checkout - and, on a live key, an **SMS** with the same link 3. The buyer taps the link, picks a wallet on the PayBridgeNP checkout page (eSewa / Khalti / Fonepay), and completes the payment 4. The Shopify order automatically flips to **Paid** within \~5 seconds of the wallet confirming In test mode (`sk_test_` key), customer SMS and email notifications are recorded but not delivered. A Billing email can reach only your own PayBridgeNP account email, which does not apply to ordinary Shopify checkout reminders. You can inspect the rendered SMS under **Account → SMS** with status `skipped_sandbox`. Missing test-mode notifications are not a broken install. If anything above doesn't happen, jump to [Troubleshooting](/integrations/shopify/troubleshooting). ### Sandbox test credentials If your PayBridgeNP API key starts with `sk_test_`, use these built-in test credentials on the PayBridgeNP hosted checkout: | Provider | Credential | | ---------- | ---------------------------------------------------------------------- | | **eSewa** | ID `9806800001`, password `Nepal@123`, MPIN `1122`, Token/OTP `123456` | | **Khalti** | Phone `9800000001`, MPIN `1111`, OTP `987654` | These credentials cover the eSewa and Khalti test environments. Fonepay has no sandbox: enabling it on a sandbox project uses your real Fonepay account and moves real money within the [sandbox caps](/guides/sandbox-testing). ## Protected customer data PayBridgeNP for Shopify reads the customer's email, phone, and name from each order so we can send the payment-link SMS and email. We also read the shipping address (and, when address collection is enabled on the checkout, write it back to the order). We do not read order history beyond the current order, do not use the data for marketing, and do not run automated profiling. Customer email, phone, name, and shipping address are stored encrypted at rest with per-shop access isolation, and automatically nulled out 90 days after the order was created, once it has reached a terminal state (paid, cancelled, or expired). This satisfies Shopify's [Protected customer data](https://shopify.dev/docs/apps/launch/protected-customer-data) and data-minimization requirements. ## What's next * [How it works](/integrations/shopify/how-it-works) - the end-to-end payment and refund flows explained * [Troubleshooting](/integrations/shopify/troubleshooting) - common setup issues and how to fix them # Shopify Source: https://docs.paybridgenp.com/integrations/shopify/overview Accept eSewa, Khalti, and Fonepay payments on your Shopify store through PayBridgeNP. PayBridgeNP for Shopify lets Nepali merchants accept local digital wallet payments (eSewa, Khalti, Fonepay) on any Shopify store, on any Shopify plan. ## Who this is for * You run a Shopify store with customers in Nepal * You want to accept **eSewa**, **Khalti**, or **Fonepay** - the three wallets most Nepali customers actually use * You already have (or can create) a [PayBridgeNP account](https://dashboard.paybridgenp.com/signup) with a `sk_live_…` or `sk_test_…` API key * Your prices are in **NPR** ## What it looks like The app is embedded directly inside your Shopify admin - same navigation, same theme, same keyboard shortcuts. Here's the dashboard a merchant sees after installation: PayBridgeNP dashboard embedded in Shopify admin, showing paid volume today, awaiting payments, reminders sent in the last 24 hours, and reconciliation rate over 7 days ## What you get * **Pay-by-link by SMS and email.** After the buyer completes the native Shopify checkout with the PayBridgeNP manual payment method, they get a branded SMS and email containing a PayBridgeNP payment link. Tapping the link opens a hosted checkout where they choose eSewa, Khalti, or Fonepay. * **Automatic reconciliation.** When the customer completes the PayBridgeNP payment, the Shopify order flips to **Paid** within seconds - no manual order matching. * **Customer account "Pay now" button (optional).** Add the PayBridgeNP block to your Customer Account order page so returning buyers can pay from their order history in one tap, without needing the SMS or email link. * **Reminders + auto-cancel.** If a customer places an order but doesn't pay, they get follow-up SMS and email reminders. Unpaid orders auto-cancel after the configured window with inventory restocked. * **Partial refund support.** Refunds you process in the PayBridgeNP dashboard mirror onto the Shopify order automatically - the order flips to **Partially refunded** or **Refunded** depending on the amount. * **Draft order payment links (Growth or higher).** Build a draft order in Shopify admin, click **Send payment link**, and the customer gets an SMS + email with a checkout URL. * **Privacy webhooks built in.** Handlers for Shopify's customer data request, redact, and shop redact webhooks, so customer data is exported or deleted on request. ## Where PayBridgeNP fits Shopify owns the cart, checkout, shipping, discounts, taxes, customer addresses, and the thank-you page. PayBridgeNP follows up after the order is placed - we deliver the payment link by SMS and email, host the wallet-picker checkout, and mark the order Paid when the wallet confirms. Everything else on your store stays 100% native Shopify. ## What this integration does NOT do Be aware of the following limitations before installing: * **No checkout customization.** PayBridgeNP does not modify or replace Shopify's checkout flow. Customers complete the standard Shopify checkout first, then receive the PayBridgeNP payment link by SMS and email. This is by design - it keeps the integration fully compliant with Shopify's API Terms. * **No refund-from-Shopify-admin.** Shopify's manual payment methods don't support in-admin refunds. Refunds must be processed from the [PayBridgeNP dashboard](https://dashboard.paybridgenp.com); the Shopify order is updated automatically afterwards. * **NPR only.** If your store's currency is not NPR, orders placed with the PayBridgeNP payment method are skipped (logged server-side - no payment link is sent). No order note is written to the Shopify order today, so check the order currency if a PayBridgeNP order never gets a link. Multi-currency support is on the roadmap. * **No subscriptions.** One-off payments only. If you need recurring billing, use PayBridgeNP's [billing plans](/api-reference/billing/plans/create-plan) directly from your backend. * **No Nepali-3DS.** None of the supported wallets use 3DS, so this isn't actually a gap - just noting it for anyone migrating from a card processor. ## Ready to install Next: [Install the app](/integrations/shopify/install). # Shopify Troubleshooting Source: https://docs.paybridgenp.com/integrations/shopify/troubleshooting Common issues with PayBridgeNP for Shopify and how to fix them. Every issue on this page has been hit in production at least once. If you run into something not listed here, [contact PayBridgeNP support](https://paybridgenp.com/contact) with your shop domain and the approximate time the problem occurred so we can cross-reference the app logs. ## An order never got a payment link (no SMS or email at all) **Most likely cause:** your **Payment method name** field in the app Settings doesn't match the Shopify payment method name, so the order webhook was ignored before any link was created. The match is case-insensitive but typo- and whitespace-sensitive. `Pay with eSewa, Khalti, or Fonepay` and `pay with esewa, khalti, or fonepay` both work; `Pay with eSewa, Khalti or Fonepay` (missing comma) or `Pay with eSewa, Khalti, or Fonepay` (double space) do not. A non-NPR order currency is ignored for the same reason (see [NPR only](/integrations/shopify/overview)). **Fix:** 1. Go to your Shopify admin → **Settings → Payments** → **Manual payment methods** 2. Note the **exact display name** of the manual method you created (e.g. `Pay with eSewa, Khalti, or Fonepay`) 3. Open the embedded PayBridgeNP app → **Settings** 4. Paste that exact name into the **Payment method name** field 5. Save, then place a fresh test order to verify ## An order got a link and was paid, but is still Pending in Shopify This means the customer paid on PayBridgeNP but the reconciliation webhook didn't flip the Shopify order. The link existed, so a payment-method-name mismatch is **not** the cause here. **Most likely cause:** the PayBridgeNP webhook connection broke - usually because the API key was **rotated or re-saved**, which re-registers the webhook, or the key was revoked. **Fix:** 1. Open the embedded PayBridgeNP app → **Settings** 2. Re-paste your current `sk_live_` (or `sk_test_`) key and **Save** - this re-registers the reconciliation webhook 3. Confirm the payment shows as succeeded in the [PayBridgeNP dashboard](https://dashboard.paybridgenp.com); if it did, the order will reconcile on the next successful delivery 4. If it still doesn't flip, [contact support](https://paybridgenp.com/contact) with the order number - the reconciler can be re-run ## Email didn't send but SMS did **Most likely cause:** the customer's order doesn't have an email address recorded in Shopify. **Fix:** 1. Open the order in your **Shopify admin** 2. Check whether an email address is shown under the customer details 3. If there's no email, the customer checked out as a guest without providing one - there's nothing to send to. You can manually add an email to the customer profile for future orders. If the order does have an email address and it still didn't arrive, [contact PayBridgeNP support](https://paybridgenp.com/contact) with the order number and approximate time - we'll investigate on our end. ## SMS didn't send but email did Check these causes in order - phone formatting is only the last one, and it's the least common: 1. **Test mode.** On an `sk_test_` key, customer SMS and email notifications are recorded but not delivered. This is by design - see [Install, Step 6](/integrations/shopify/install). Look for status `skipped_sandbox` in the PayBridgeNP dashboard under **Account → SMS**. 2. **Plan / quota.** Live SMS is plan-gated: Free = 0 live SMS, Growth = 200/month, Pro = 1,000/month. Once the monthly allowance is used, further sends are skipped. A temporary `skipped_rate_limited` result means you should wait before retrying. 3. **Template turned off.** If the payment SMS template is disabled for the merchant, the send is skipped (`skipped_template_disabled`). 4. **Phone format.** The phone number must be a Nepali mobile the provider recognizes. The app accepts: * `9800000000` - 10 digits starting with 96, 97, 98, or 99 * `+9779800000000` - with the 977 country code * `977-9800000000` - with dash separator Anything else (US numbers, Indian numbers, landlines) is skipped (`skipped_invalid_phone`). The **SMS log** in the dashboard shows the exact skip reason on every row - start there rather than guessing. ## "Return to merchant" from PayBridgeNP checkout shows "Invalid request" **Most likely cause:** browser cached the PayBridgeNP checkout success page from an older version of the app that built malformed return URLs. **Fix:** 1. On the PayBridgeNP checkout success page, press **⌘⇧R** (Mac) or **Ctrl+Shift+R** (Windows/Linux) for a hard refresh 2. Click **Return to merchant** again If it still fails after a hard refresh, the return URL itself is malformed - [contact support](https://paybridgenp.com/contact) with the full URL and we'll debug. ## The embedded app loads but shows a 500 error **Most likely cause:** a recent update to the app introduced a change that requires a fix on our end. **Fix:** [Contact PayBridgeNP support](https://paybridgenp.com/contact) with your shop domain and the approximate time the error started - this is something we fix on our side, not from your admin UI. ## The app says "Shopify session expired" randomly **Most likely cause:** this is expected - Shopify sessions are short-lived, and the app refreshes them automatically. You should never see this message in normal use. If you see it anyway, it usually means your session was invalidated - for example, you revoked the app's access in Shopify admin and then reinstalled it. Clicking refresh or navigating to any embedded-app page re-establishes the session automatically. ## Draft order "Send payment link" button is disabled **Most likely cause:** the draft order's customer has neither an email nor a phone number. **Fix:** 1. Open the draft order in Shopify admin 2. Click **Customer → Edit** and add either an email or a phone number (Nepali format for phone) 3. Save the draft 4. Refresh the Draft orders list in the embedded PayBridgeNP app - the button should now be enabled ## Still stuck? If none of the above describes your issue, [contact PayBridgeNP support](https://paybridgenp.com/contact) with: * Your shop domain (`yourstore.myshopify.com`) * The approximate time the issue occurred (with timezone) * The order number or PayBridgeNP session id if applicable * A screenshot of anything visibly broken We'll pull the app logs for that timeframe and respond within one business day. # How the WHMCS Integration Works Source: https://docs.paybridgenp.com/integrations/whmcs/how-it-works The PayBridgeNP for WHMCS payment flow, invoice state machine, refund path, and security model explained. This page explains what happens inside the module when a customer pays an invoice, how WHMCS tracks the state transitions, and where each piece of the handshake lives. Read it once and you'll know exactly which piece to look at when something doesn't behave as expected. ## The payment flow The customer opens an unpaid invoice in the client area and clicks the **Pay Now** button rendered by `paybridgenp_link()`. WHMCS posts back to the same invoice URL with a hidden flag that tells the gateway to create a checkout session. Inside `paybridgenp_link()` with the redirect flag set, the module: * Reads the invoice total and converts it to paisa (`total × 100`, rounded) via `Amount::toPaisa()` * Builds the return URL and cancel URL - both point at `modules/gateways/callback/paybridgenp.php?invoiceid=`, using the **Public Callback URL** if set or the WHMCS **System URL** as fallback * Calls `PayBridgeNP::checkout->create()` via the PayBridgeNP PHP SDK with `amount`, `currency=NPR`, `return_url`, `cancel_url`, and metadata (invoice id, client id, client email, WHMCS version, `source=whmcs`) * Writes a `checkout.created` entry to the Gateway Log with the session id If the PayBridgeNP API call fails (bad API key, API down, etc.), the module renders an inline error on the invoice page and logs `checkout.api_error` to the Gateway Log. On success, it returns HTML that redirects the browser to the hosted checkout URL via `` + `` + a manual fallback link. The customer lands on `https://checkout.paybridgenp.com/checkout/cs_…`, sees your merchant name and the invoice total, picks an available **eSewa**, **Khalti**, or configured **Fonepay** method, and completes the payment. After the wallet confirms the payment, PayBridgeNP redirects the customer's browser back to: ``` https://your-whmcs-url/modules/gateways/callback/paybridgenp.php?invoiceid=&session_id=cs_…&status=success&payment_id=pay_… ``` The callback file detects this is a browser return (no `X-PayBridgeNP-Signature` header) and calls `paybridgenp_handle_return()`, which: * Validates the invoice id * Redirects the customer to the WHMCS invoice view with a flash query param (`?paybridge_status=submitted` / `cancelled` / `failed`) * **Does not touch invoice state** - the webhook (next step) is the only authoritative confirmation **Why no state change here?** Because the browser return is *not* trustworthy - an attacker could craft a return URL with a fake `status=success` and a guessed invoice id. Only the HMAC-signed webhook can move the invoice to **Paid**. A few seconds after the wallet confirms the payment, PayBridgeNP fires a signed `payment.succeeded` webhook to the same callback URL: ``` POST https://your-whmcs-url/modules/gateways/callback/paybridgenp.php X-PayBridgeNP-Signature: t=,v1= Content-Type: application/json { "id": "evt_…", "type": "payment.succeeded", "data": { "id": "pay_…", "amount": 10000, "metadata": { "invoiceid": "42", ... } } } ``` The callback detects the signature header and calls `paybridgenp_handle_webhook()`, which: * Reads the raw request body and the `X-PayBridgeNP-Signature` header * Looks up the webhook signing secret from gateway settings - **refuses with HTTP 400 if the secret is not configured** (no unsigned fallback) * Verifies the HMAC-SHA256 signature via the PayBridgeNP PHP SDK's `constructEvent()` - rejects forged events and replays older than 5 minutes * Extracts the invoice id from `event.data.metadata.invoiceid` * Calls WHMCS's `checkCbInvoiceID()` to ensure the invoice exists and belongs to this gateway * Calls WHMCS's `checkCbTransID()` to dedupe by PayBridgeNP payment id - if the same webhook is replayed, this exits early before any double-apply * Calls `addInvoicePayment()` with the amount converted back to rupees, the payment id as the transaction id, and zero fees * Writes a `webhook.payment_applied` entry to the Gateway Log with the provider, payment id, and amount * Returns HTTP 200 `OK` WHMCS does the rest: the invoice flips to **Paid**, the balance zeros out, any associated service is provisioned, and WHMCS's invoice-paid email goes to the customer. ## Invoice state transitions | State | When | Who sets it | | ----------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | `Draft` | Invoice created by admin, not yet visible to client | WHMCS core | | `Unpaid` | Published invoice awaiting payment | WHMCS core | | `Payment Pending` | Optional state some WHMCS flows use while awaiting async confirmation | WHMCS core | | `Paid` | Webhook confirmed `payment.succeeded`, `addInvoicePayment()` settled the balance | Module's webhook handler | | `Refunded` | Full refund recorded against the original transaction | WHMCS core (after `paybridgenp_refund()` returns `status=success`) | | `Cancelled` | Admin-cancelled before payment cleared | WHMCS core | ## What the webhook handler does for each event type | Event | Action (if unpaid) | Action (if already paid) | | ------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------- | | `payment.succeeded` | → `addInvoicePayment()` applies the payment, invoice becomes **Paid** | No change - `checkCbTransID()` silently bails on duplicate | | `payment.failed` | Logged to Gateway Log as `webhook.payment_failed`; no invoice state change | Same | | `payment.refunded` | Logged as `webhook.ignored` (refund state is not applied from webhooks) | Same | | Any other type | Logged as `webhook.ignored` with the event type recorded | Same | `payment.failed` deliberately doesn't move the invoice to a failed state - WHMCS keeps it Unpaid so the customer can retry. The failure is recorded in the Gateway Log for merchant visibility. PayBridgeNP also emits `payment.cancelled` when the buyer backs out on the provider page; the module logs it without changing the invoice state, so subscribing to it is optional. ## The refund path When an admin clicks **Refund** on a transaction tied to the PayBridgeNP gateway, WHMCS calls `paybridgenp_refund()` with the transaction id and the amount to refund. The module: 1. Maps the refund amount to paisa 2. Calls `PayBridgeNP::refunds->create()` with `paymentId`, `amount` (paisa), `reason=other`, and a `notes` field containing `"Refund issued from WHMCS admin (invoice #)"` 3. Logs the result to the Gateway Log 4. Reports the result to WHMCS * **Khalti:** A completed refund is reported to WHMCS as `status=success`, with the PayBridgeNP refund id. WHMCS then records the refund against the original transaction and adjusts the invoice totals. Partial refunds use the same path. * **eSewa:** PayBridgeNP records the refund as `requires_action`. The module returns `status=declined` with a message telling you to pay it out in the eSewa merchant portal, then mark the invoice refunded in WHMCS. Partial refunds use the same path. * **Fonepay:** The create-refund API refuses the request, so the module returns `status=declined`. Refund the payment from the [PayBridgeNP dashboard](https://dashboard.paybridgenp.com), then record the result in WHMCS manually. Failed responses and API errors are also reported to WHMCS as `status=declined`. Any other refund response is returned as `status=success`, with its refund id. **eSewa refunds need manual action.** The refund is recorded in PayBridgeNP as `requires_action`, but it is not finalised in WHMCS automatically. The module returns `status=declined` and tells you to pay it out in the eSewa merchant portal, then mark the invoice refunded in WHMCS. PayBridgeNP's refund API requires `reason` to be one of `customer_request | duplicate | fraudulent | other`. The module defaults to `other` and puts the WHMCS-specific context in the `notes` field, which is stored with the refund in PayBridgeNP. ## Where the module lives in your WHMCS install After extraction, the module's files are under: ``` modules/gateways/ ├── paybridgenp.php ← main gateway (MetaData, config, link, refund) ├── paybridgenp/ │ ├── init.php ← composer autoloader bootstrap │ ├── whmcs.json ← manifest for Apps & Integrations modal │ ├── logo.png ← shown next to the gateway name │ ├── src/ │ │ ├── Amount.php ← paisa ↔ rupees conversion │ │ ├── CheckoutParams.php ← WHMCS params → checkout request │ │ ├── Config.php ← normalised settings accessor │ │ └── Logger.php ← gateway-log wrapper with secret scrubbing │ └── vendor/ ← vendored PayBridgeNP PHP SDK └── callback/ └── paybridgenp.php ← return URL + webhook handler ``` The `vendor/` directory is bundled inside the ZIP - you do NOT need to run `composer install` on your server. ## The Gateway Log Every interaction flows through WHMCS's standard `logTransaction()` helper, which surfaces under **Utilities → Logs → Gateway Log**: WHMCS Gateway Log showing PayBridgeNP events Event types you'll see: | Event | Meaning | | ---------------------------------------- | ----------------------------------------------------------------------- | | `checkout.created` | Customer clicked Pay Now, session created successfully | | `checkout.request` / `checkout.response` | Full API body (only logged when **Debug Logging** is on) | | `checkout.api_error` | PayBridgeNP rejected the checkout creation - check the `message` field | | `webhook.payment_applied` | Invoice successfully marked paid from a verified webhook | | `webhook.payment_failed` | `payment.failed` or `payment.cancelled` event delivered | | `webhook.invalid_signature` | HMAC verification failed - either the wrong secret or a forgery attempt | | `webhook.no_secret_configured` | Someone fired a webhook but no signing secret is saved | | `webhook.ignored` | Event type we don't act on (e.g. `payment.refunded`) | | `refund.*` | Admin-initiated refund result | | `return.hit` | Browser return URL was hit (only logged when **Debug Logging** is on) | API keys, the webhook signing secret, and the `X-PayBridgeNP-Signature` header value are automatically redacted before they land in the log - safe to share with support without exposing credentials. ## Security model * **Webhook signature verification is mandatory.** The module refuses to process webhooks unless the signing secret is configured (HTTP 400 response). There is no unsigned fallback. * **5-minute replay window.** HMAC verification rejects events older than 5 minutes based on the `t=` timestamp in the signature header. * **`checkCbInvoiceID` + `checkCbTransID`** are WHMCS built-ins that prevent cross-gateway payment application and duplicate postings. * **`logTransaction` scrubbing.** The module's `Logger` class redacts `api_key`, `secretKey`, `testSecretKey`, `webhookSecret`, `signing_secret`, `authorization`, and `X-PayBridgeNP-Signature` before passing data to `logTransaction()`. * **No raw SQL anywhere.** All invoice operations go through WHMCS's `addInvoicePayment()` / helper functions. * **`hash_equals` comparisons** inside the SDK for signature matching - timing-safe. ## What's next * [Troubleshooting](/integrations/whmcs/troubleshooting) - common issues and how to fix them * [Back to installation](/integrations/whmcs/install) if you haven't set up the gateway yet # Install the WHMCS module Source: https://docs.paybridgenp.com/integrations/whmcs/install Download, extract, activate, and configure PayBridgeNP for WHMCS. Installing the PayBridgeNP WHMCS module takes about 10 minutes. You'll download a ZIP, extract it into your WHMCS install, activate the gateway from **Apps & Integrations**, and paste your PayBridgeNP test or live key plus the webhook signing secret. ## Before you start You need: 1. A **WHMCS** installation (version **8.0 or newer**) running on **PHP 7.4 or newer** with the **ionCube Loader** extension enabled 2. Your invoice currency set to **NPR** (Nepalese Rupee) in **Configuration → System Settings → Currencies** - this module only sends NPR amounts 3. A **PayBridgeNP API key** from the [PayBridgeNP dashboard](https://dashboard.paybridgenp.com) under **Settings → API Keys** (either `sk_live_…` or `sk_test_…`) 4. Administrator access to your WHMCS admin area and SSH / SFTP access to the WHMCS install directory on your server ## Step 1 - Download the module Download the latest release from [paybridgenp.com/integrations/whmcs](https://paybridgenp.com/integrations/whmcs) by clicking the **Download module** button, or find us on the [WHMCS Marketplace](https://marketplace.whmcs.com/product/8648). You'll get a file named: ``` paybridgenp-whmcs-.zip ``` This is a \~85 KB archive - the PayBridgeNP PHP SDK is vendored inside, so you don't need Composer on your server. If you prefer to pull it from the command line: ```bash theme={null} curl -LO https://paybridgenp.com/downloads/paybridgenp-whmcs-latest.zip ``` ## Step 2 - Extract into your WHMCS install The ZIP is structured to drop into the root of your WHMCS install. From the server's WHMCS directory (the one containing `configuration.php`, `admin/`, `modules/`, etc.): ```bash theme={null} cd /path/to/whmcs unzip paybridgenp-whmcs-.zip ``` The archive adds three things under `modules/gateways/`: ``` modules/gateways/ ├── paybridgenp.php ← main gateway file ├── paybridgenp/ ← helpers + vendored SDK │ ├── init.php │ ├── src/ │ ├── vendor/ │ ├── logo.png │ └── whmcs.json └── callback/paybridgenp.php ← return URL + webhook handler ``` Do NOT upload the ZIP through the WHMCS admin UI - WHMCS's built-in uploader is for addon modules, not gateway modules. Gateway modules must be placed on the filesystem directly. If you don't have SSH / SFTP access, use your hosting panel's **File Manager** to unzip it in place. ## Step 3 - Activate the gateway 1. Log in to your WHMCS admin area 2. Open the gateway list: * **WHMCS 9.0 and newer:** Configuration → **Apps & Integrations** → **Browse** → **Payments** * **WHMCS 8.x:** Configuration → System Settings → **Payment Gateways** → **All Payment Gateways** tab 3. Find **PayBridgeNP** in the list and click the tile PayBridgeNP tile inside WHMCS Apps & Integrations 5. Click **Activate** (or **Manage** if it's already activated) WHMCS redirects you to the gateway configuration form. ## Step 4 - Configure the gateway Fill in the form: PayBridgeNP WHMCS gateway configuration form | Field | Value | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Show on Order Form** | ✅ check to expose PayBridgeNP on client-facing checkout | | **Display Name** | `PayBridgeNP` (or whatever you want customers to see, e.g. `Pay with eSewa, Khalti, or Fonepay`) | | **Live Secret Key** | Paste your `sk_live_…` key. Leave blank while you're testing with `sk_test_…`. | | **Test Secret Key** | Paste your `sk_test_…` key. Used when **Test Mode** is enabled. | | **Test Mode** | Check while you're testing - routes all API calls to the sandbox using the test key above | | **Webhook Signing Secret** | Your `whsec_…` - see [Step 5](#step-5-register-the-webhook) | | **Public Callback URL** | Usually leave blank. Fill only if your WHMCS is behind a load balancer, a different public hostname, or you're using a tunnel in local dev. Example: `https://tunnel.example.com/` | | **Payment Method** | Leave as **Let payer choose from available methods** - the hosted page shows eSewa, Khalti, and configured Fonepay. Or force one method if you only want one visible. | | **Debug Logging** | On while testing - writes full API request/response bodies to the Gateway Log. Turn off in production. | Don't click **Save Changes** yet - you still need the webhook signing secret in the next step. ## Step 5 - Register the webhook The webhook is **required**. Without it, customers can successfully pay on the PayBridgeNP hosted checkout but the WHMCS invoice will stay **Unpaid** - WHMCS has no way to know the payment completed. 1. In your [PayBridgeNP dashboard](https://dashboard.paybridgenp.com), go to **Webhooks → Add endpoint** 2. Set the **URL** to the full callback path on your WHMCS install: ``` https://your-whmcs-url/modules/gateways/callback/paybridgenp.php ``` **Use the full path, not just the domain.** If you paste only the base URL (`https://your-whmcs-url/`), PayBridgeNP will POST webhooks to your WHMCS homepage, which returns 200 but ignores the payload - the invoice never gets marked paid. 3. **Events to subscribe to:** * `payment.succeeded` * `payment.failed` 4. Click **Create endpoint** 5. PayBridgeNP shows you the signing secret (`whsec_…`) **exactly once**. Copy it immediately. 6. Paste it into the **Webhook Signing Secret** field in the gateway config from Step 4, then click **Save Changes** ## Step 6 - Place a test invoice Switch to a non-admin browser (or use an incognito window) so you see the client area like a real customer does. 1. In WHMCS admin, go to **Clients → Add New Client** and create a minimal client record 2. Open that client → **Invoices** tab → **Create New Invoice** - add a line item (e.g. `Test` for `Rs. 100.00`), set **Payment Method** to `PayBridgeNP` 3. Click **Publish** - drafts don't show a Pay button on the client side 4. Click **View as Client** (or use the client-area login link) You should see an invoice with a purple **Pay Now** button: Client-area invoice with Pay Now button ### What should happen 1. Clicking **Pay Now** creates a PayBridgeNP checkout session and redirects you to the hosted payment page 2. Pick an available **eSewa**, **Khalti**, or configured **Fonepay** method on the hosted page and complete the payment (sandbox credentials listed below) 3. PayBridgeNP redirects you back to the WHMCS invoice view 4. Within a few seconds the signed webhook arrives and the invoice flips to **Paid** - a transaction row appears under **Transactions** with the PayBridgeNP payment id (`pay_…`) If the invoice stays **Unpaid** longer than 30 seconds, the webhook didn't arrive - jump to [Troubleshooting: webhook not applied](/integrations/whmcs/troubleshooting#webhook-returns-200-but-invoice-stays-unpaid). ### Sandbox test credentials If you used a `sk_test_…` key, use the built-in sandbox credentials on the PayBridgeNP hosted checkout: | Provider | Credential | | ---------- | --------------------------------------------- | | **eSewa** | Merchant code `EPAYTEST`, any amount | | **Khalti** | Phone `9800000001`, MPIN `1111`, OTP `987654` | These credentials cover the eSewa and Khalti test environments. Fonepay has no sandbox: enabling it on a sandbox project uses your real Fonepay account and moves real money within the [sandbox caps](/guides/sandbox-testing). ## Local development webhook testing If you're running WHMCS on `http://localhost` (e.g. inside Docker), PayBridgeNP can't reach it directly. Use a tunnel: ```bash theme={null} cloudflared tunnel --url http://localhost:8080 # or ngrok http 8080 ``` Then paste the public `https://…trycloudflare.com/` URL into the **Public Callback URL** field in your gateway settings. This lets our module generate return/webhook URLs against the tunnel without you having to change the WHMCS **System URL** (which affects admin navigation). Register the webhook in your PayBridgeNP dashboard against the same tunnel URL - full path, as always: ``` https://.trycloudflare.com/modules/gateways/callback/paybridgenp.php ``` ## Updating the module When a new version ships: 1. Download the latest ZIP from [paybridgenp.com/integrations/whmcs](https://paybridgenp.com/integrations/whmcs) or the [WHMCS Marketplace](https://marketplace.whmcs.com/product/8648) 2. Extract it into the root of your WHMCS install - files overwrite in place; your gateway config persists because it's stored in `tblpaymentgateways`, not on disk No deactivation / reactivation dance needed. ## What's next * [How it works](/integrations/whmcs/how-it-works) - payment flow, invoice state transitions, security model * [Troubleshooting](/integrations/whmcs/troubleshooting) - common issues and how to fix them # WHMCS Source: https://docs.paybridgenp.com/integrations/whmcs/overview Accept eSewa, Khalti, and configured Fonepay payments inside WHMCS through a single PayBridgeNP integration. PayBridgeNP for WHMCS is a first-party payment gateway module that adds a **PayBridgeNP** method to your WHMCS billing panel. Customers pick it like any other gateway, get redirected to the PayBridgeNP hosted checkout, choose from their available eSewa, Khalti, and Fonepay methods, and land back on the invoice page - the invoice flips to **Paid** automatically via a signed webhook. ## Who this is for * You run a **WHMCS** billing panel (8.0 or newer) for hosting, domains, or any recurring service * You want to accept **eSewa**, **Khalti**, and configured **Fonepay** from Nepali customers in one module - no separate integrations to reconcile * You already have (or can create) a [PayBridgeNP account](https://dashboard.paybridgenp.com/signup) with a `sk_live_…` or `sk_test_…` API key * Your invoice currency is **NPR** ## What you get * **eSewa + Khalti + Fonepay in one module.** Let the payer choose from available methods, or force one provider. No duplicate webhook endpoints. * **Hosted checkout.** Customers pay on a mobile-friendly page served by PayBridgeNP. No iframe embedding, no card input inside WHMCS - compliance surface stays small. * **Khalti and eSewa refunds from WHMCS admin.** Start Khalti and eSewa refunds from WHMCS admin, including partial refunds. A completed Khalti refund is reported to WHMCS. An eSewa refund is recorded as `requires_action`, and the module returns it as declined with instructions to pay it out in the eSewa merchant portal, then mark the invoice refunded. The create-refund API refuses Fonepay refunds, so refund those from the PayBridgeNP dashboard. * **HMAC-signed webhooks.** Every `payment.succeeded` / `payment.failed` event is verified against an HMAC-SHA256 signature with a 5-minute replay window. Forged events are rejected with a 400 response. * **Idempotent payment application.** Uses WHMCS's `checkCbTransID` helper so replayed webhooks never double-post to `tblaccounts`. * **Gateway Log integration.** Every API call, webhook delivery, and refund attempt is recorded in **Utilities → Logs → Gateway Log** with request/response context. API keys and signatures are automatically scrubbed before they land in the log - safe to share with support. * **Public Callback URL override.** Optional field for installs behind a load balancer where the WHMCS System URL isn't internet-reachable, or for local dev with a tunnel (cloudflared / ngrok). * **GPL-2.0-or-later licensed.** Same license family as WHMCS itself. ## How it plugs in ``` ┌────────────────┐ 1. pays invoice (picks PayBridgeNP) ┌───────────────┐ │ Customer │ ───────────────────────────────────────▶│ WHMCS │ └────────────────┘ └──────┬────────┘ │ 2. paybridgenp_link() │ creates checkout session ▼ ┌──────────────────┐ │ PayBridgeNP │ │ hosted checkout │ └────────┬─────────┘ │ 3. customer pays (eSewa / Khalti / Fonepay) ▼ ┌────────────────┐ 5. return URL → invoice view ┌──────────────────┐ │ Customer │ ◀───────────────────────────────────── │ callback.php │ └────────────────┘ (no state mutation here) └──────┬───────────┘ │ │ 4. signed webhook (authoritative) ▼ ┌──────────────────┐ │ callback.php │ │ → invoice Paid │ └──────────────────┘ ``` ## WHMCS Marketplace PayBridgeNP is officially listed on the WHMCS Marketplace at [marketplace.whmcs.com/product/8648](https://marketplace.whmcs.com/product/8648). The download at [paybridgenp.com/integrations/whmcs](https://paybridgenp.com/integrations/whmcs) is always the current release, so use it if you want the newest version. ## What's next * [Install the WHMCS module](/integrations/whmcs/install) - download, extract, activate, configure (about 10 minutes) * [How it works](/integrations/whmcs/how-it-works) - payment flow, invoice state transitions, webhook handling, security model * [Troubleshooting](/integrations/whmcs/troubleshooting) - common issues and how to fix them # WHMCS Troubleshooting Source: https://docs.paybridgenp.com/integrations/whmcs/troubleshooting Common issues when setting up and running the PayBridgeNP for WHMCS module, and how to fix them. If something isn't working, check the **Utilities → Logs → Gateway Log** inside WHMCS admin first. Every API call, webhook delivery, and refund attempt is recorded there with a structured payload. Most issues in this guide point you at the matching Gateway Log entry. ## Webhook returns 200 but invoice stays Unpaid **Symptom:** The PayBridgeNP dashboard shows a `payment.succeeded` webhook delivered with a 200 response, but the WHMCS invoice never flips to **Paid**. **Almost always the cause:** the webhook URL in the PayBridgeNP dashboard was registered as just the domain root instead of the full callback path. PayBridgeNP POSTs to `https://your-whmcs-url/` (your WHMCS homepage), which returns 200 but ignores the payload. **Fix:** In the PayBridgeNP dashboard, edit the webhook endpoint and change the URL to the full callback path: ``` https://your-whmcs-url/modules/gateways/callback/paybridgenp.php ``` Then click **Replay** on the original delivery - our callback will verify the HMAC, resolve the invoice from metadata, and flip it to Paid. ## Refund fails with "reason must be one of..." **Symptom:** Clicking **Refund via Gateway** shows "Refund Failed" and the Gateway Log contains: ``` event => refund.api_error message => reason must be one of: customer_request, duplicate, fraudulent, other ``` **Cause:** You're running module version **0.1.0** or earlier, which sent a freeform string as the `reason` field. **Fix:** Upgrade to **0.1.1 or later**. Version 0.1.1 sends `reason=other` and puts the WHMCS context in the `notes` field, matching the PayBridgeNP API's enum requirement. ## Gateway doesn't show up in Apps & Integrations **Symptom:** After extracting the ZIP, **PayBridgeNP** doesn't appear in the Apps & Integrations list. **Things to check:** 1. The `modules/gateways/paybridgenp.php` file is present and readable by your web server user 2. The `modules/gateways/paybridgenp/vendor/autoload.php` file exists - if you copied only the `.php` file without the `paybridgenp/` directory, the module will throw a fatal on load. Re-extract the full ZIP. 3. PHP version is 7.4 or newer and **ionCube Loader** is enabled (run `php -v` on the server - you should see "ionCube PHP Loader") 4. Clear the WHMCS template cache: ```bash theme={null} rm -f /path/to/whmcs/templates_c/*.php ``` ## "Your license key is invalid" after changing servers **Symptom:** WHMCS redirects everything to `/admin/licenseerror.php?status=Invalid`. The error page shows: ``` Valid Domains: Valid IPs: ``` …but your current IP or domain doesn't match. **Cause:** WHMCS licenses are bound to the server's IP + domain + install directory. When any of those change (e.g. migrating Docker images, moving hosts, renaming domains), the license check fails. **Fix:** Log in to [clientarea.whmcs.com](https://www.whmcs.com/members/clientarea.php) and click **Reissue License** on the product detail page. The next request from the new environment re-registers. ## Invoice payment gets redirected to [https://localhost](https://localhost) (connection refused) **Symptom:** Admin pages redirect to `https://localhost/...` and the browser shows **ERR\_CONNECTION\_REFUSED**. **Cause:** During install, WHMCS saved `SystemURL` as `https://localhost/` - now every admin navigation forces HTTPS redirect, but your container only serves HTTP. **Fix:** Update `tblconfiguration.SystemURL` directly in the database: ```sql theme={null} UPDATE tblconfiguration SET value='http://localhost:8080/' WHERE setting='SystemURL'; ``` Then reload `/admin/` without the trailing `https://`. ## Webhook rejected with "Invalid signature" **Symptom:** Gateway Log shows `webhook.invalid_signature` for every incoming webhook. **Cause:** The **Webhook Signing Secret** in your gateway config does not match the secret issued when the webhook endpoint was registered in the PayBridgeNP dashboard. **Fix:** 1. In the PayBridgeNP dashboard, delete the webhook endpoint 2. Re-create it - PayBridgeNP shows the new signing secret (`whsec_…`) exactly once 3. Paste the new secret into the **Webhook Signing Secret** field in WHMCS and save Signing secrets can never be retrieved after the initial display - always copy them immediately. ## Checkout creation fails with HTTP 400 **Symptom:** Clicking Pay Now shows "Could not start payment: …" and the Gateway Log shows `checkout.api_error`. **Common causes:** * **Currency is not NPR.** The module rejects invoices in any other currency. Change your store default or set the invoice currency to NPR. * **API key is blank or wrong.** Confirm you pasted a `sk_test_…` or `sk_live_…` key and there's no stray whitespace. * **Test Mode toggle doesn't match the key type.** If you're using a `sk_test_…` key, check **Test Mode** to on. If you're using `sk_live_…`, leave it off. ## Invoice stays on the "submitted" state forever **Symptom:** Customer completes payment, returns to WHMCS, invoice shows a `paybridge_status=submitted` in the URL but still says **Unpaid**, and no webhook shows up in the PayBridgeNP dashboard. **Things to check:** 1. **Is the webhook endpoint reachable from the internet?** For local dev, you need a tunnel (cloudflared or ngrok) and the **Public Callback URL** field must point at the tunnel URL. 2. **Is the webhook endpoint registered with the right events?** It must subscribe to `payment.succeeded` at minimum. 3. **Check the PayBridgeNP dashboard's webhook delivery log** for the delivery attempt - if PayBridgeNP is not even trying to deliver, the endpoint might be disabled or the URL might be malformed. ## Debug Logging captures too much - noisy Gateway Log **Symptom:** Every Pay Now click writes three entries, and the Gateway Log fills up. **Cause:** `Debug Logging` is enabled. That's fine during setup but overkill in production - debug entries include full API request/response bodies for every checkout and webhook. **Fix:** In **Configuration → System Settings → Payment Gateways → PayBridgeNP**, turn off **Debug Logging** and save. The module still logs every important event (`checkout.created`, `webhook.payment_applied`, `refund.success`, all error types) - debug entries are only the raw HTTP bodies. ## Still stuck? If none of the above fit your issue: 1. Enable **Debug Logging** in the gateway config 2. Reproduce the issue 3. Export the matching Gateway Log entries (remove any `message` / `data` fields that still contain sensitive info - secrets are scrubbed automatically but always double-check before sharing) 4. Contact us via [paybridgenp.com/contact](https://paybridgenp.com/contact) with the payment id (`pay_…`) or checkout session id (`cs_…`) and the log excerpts ## What's next * [Back to installation](/integrations/whmcs/install) * [How it works](/integrations/whmcs/how-it-works) - flow + security details # How the WooCommerce Integration Works Source: https://docs.paybridgenp.com/integrations/woocommerce/how-it-works The PayBridgeNP for WooCommerce payment flow, order states, and webhook handling explained. This page explains what happens when a customer places an order, what WooCommerce does with the order status as it moves through the flow, and how PayBridgeNP confirms payment. Read it once and you'll know which part of the flow to check when something doesn't behave as expected. ## The payment flow The customer reaches your WooCommerce checkout (classic shortcode or Blocks - both work), picks **PayBridgeNP** as the payment method, and clicks **Place order**. WooCommerce creates a new order in the `pending` status and asks the PayBridgeNP gateway to start checkout. The plugin: * Reads the order total and converts it to paisa (`total × 100`, rounded) * Builds the return and cancel URLs for your store * Creates a PayBridgeNP checkout session with the amount, currency (NPR), return URL, cancel URL, and order metadata * Records the PayBridgeNP session reference on the order for traceability * Adds an order note for support/debugging If the PayBridgeNP API call fails (bad API key, API down, etc.), the plugin adds a checkout notice with the error message and returns a failure result - the customer sees the error on the checkout page and can try again. On success, WooCommerce redirects the customer's browser to PayBridgeNP's hosted checkout. The customer lands on `https://checkout.paybridgenp.com/checkout/cs_...`, sees your merchant name and the order total, picks a wallet (eSewa, Khalti, or Fonepay), and completes the payment. After the wallet confirms the payment, PayBridgeNP redirects the customer's browser back to your store with the session reference and success status. The return handler: * Validates the order id + order key (using `hash_equals` to prevent timing attacks and cross-order tampering) * If the order is already paid (webhook arrived before the browser), redirects straight to the order-received page * Otherwise, moves the order to **On hold** with a note ("PayBridgeNP: payment submitted, awaiting server confirmation") * Redirects the customer to WooCommerce's standard order-received page **Why On hold and not Processing?** Because the browser return is *not* authoritative - an attacker could forge a `return` URL with a valid order key and a fake `status=success`. The webhook (next step) is the only trustworthy confirmation. A few seconds after the wallet confirms the payment, PayBridgeNP sends a signed `payment.succeeded` webhook to your store. The webhook handler: * Reads the raw request body and the `X-PayBridgeNP-Signature` header * Looks up the webhook signing secret from gateway settings - **refuses with HTTP 400 if the secret is not configured** (no unsigned fallback) * Verifies the HMAC-SHA256 signature via the PayBridgeNP PHP SDK's `constructEvent()` - rejects forged events and replays older than 5 minutes * Extracts the order id from `event.data.metadata.order_id` * Validates the order key from `event.data.metadata.order_key` against the stored order key (same `hash_equals` guard as the return handler) * Marks the WooCommerce order as paid, which: * Sets the order to **Processing** (or **Completed** for virtual/downloadable products) * Records the PayBridgeNP payment id as the transaction id (visible in WC admin under the order) * Reduces stock levels * Lets WooCommerce run its normal post-payment notifications and fulfillment hooks * Adds an order note recording the provider name, provider reference, and amount * Returns HTTP 200 `OK` ## Order state machine The plugin moves orders through these WooCommerce states: | State | When | Who sets it | | ------------ | --------------------------------------------------------------------------------------------------------- | -------------------------------------- | | `pending` | Order created, customer hasn't returned from PayBridgeNP yet | WooCommerce core | | `on-hold` | Customer returned successfully, webhook hasn't arrived yet | PayBridgeNP return handling | | `processing` | Webhook confirmed `payment.succeeded`, transaction id recorded | PayBridgeNP webhook handling | | `cancelled` | Customer clicked cancel on PayBridgeNP's hosted checkout | PayBridgeNP return handling | | `failed` | Customer returned failed, OR webhook delivered `payment.failed` / `payment.cancelled` for an unpaid order | PayBridgeNP return or webhook handling | | `completed` | Fulfillment complete (virtual/downloadable products auto-complete on `payment_complete`) | WooCommerce core | ## What the webhook handler does for each event type | Event | Order state (if unpaid) | Order state (if already paid) | | ------------------- | ----------------------------------------------------------- | ----------------------------- | | `payment.succeeded` | → `processing` through WooCommerce's normal paid-order flow | No change - idempotent skip | | `payment.failed` | → `failed` with the reason in an order note | No change - we never un-pay | | `payment.cancelled` | → `failed` with the reason in an order note | No change - we never un-pay | The `is_paid()` check before every state transition is the idempotency guard - PayBridgeNP retries webhooks, and we never want a duplicate delivery to accidentally "un-pay" an already-confirmed order. ## Block checkout vs classic checkout Both are supported out of the box. You don't have to choose one or the other - if your theme uses the classic checkout, the classic gateway renders. If your theme uses the Blocks checkout (the default for stores created since late 2023), the Blocks gateway renders. Both use the same payment flow. ## HPOS (High-Performance Order Storage) The plugin declares HPOS compatibility, so you can enable HPOS in **WooCommerce → Settings → Advanced → Features** and the plugin will continue to work. ## Security model * **Webhook signature verification is mandatory.** The plugin refuses to process webhooks unless the signing secret is configured (HTTP 400 response). There is no unsigned fallback. * **Order key validation in both the return and webhook handlers** prevents an attacker from guessing an order id and forging a success event for someone else's order. * **`hash_equals` comparison** for order keys uses a timing-safe string compare. * **`add_query_arg` + `sanitize_text_field`** for all incoming query params. * **`wp_unslash`** on every `$_GET` / `$_SERVER` read to undo WordPress's automatic slash-escaping. * **No raw SQL anywhere** - all order operations go through the WooCommerce CRUD layer. ## What's next * [Troubleshooting](/integrations/woocommerce/troubleshooting) - common issues and how to fix them * [Back to installation](/integrations/woocommerce/install) if you haven't set up the gateway yet # Install the WooCommerce plugin Source: https://docs.paybridgenp.com/integrations/woocommerce/install Download, upload, activate, and configure PayBridgeNP for WooCommerce. Installing the PayBridgeNP WooCommerce plugin takes about 5 minutes. The plugin is published on the [WordPress.org plugin directory](https://wordpress.org/plugins/paybridgenp-for-woocommerce/), so the fastest path is searching "PayBridgeNP" from your WordPress admin. ZIP upload and source install are also supported. ## Before you start You need: 1. A **WordPress** site (5.8 or newer) running **WooCommerce** (7.0 or newer) on **PHP 7.4 or newer** 2. Your store currency set to **NPR** (Nepalese Rupee) in **WooCommerce → Settings → General** 3. A **PayBridgeNP API key** from the [PayBridgeNP dashboard](https://dashboard.paybridgenp.com) under **Settings → API Keys** (either `sk_live_…` or `sk_test_…`) 4. Administrator access to your WordPress admin - the plugin upload requires `manage_options` capability ## Step 1 - Install the plugin Pick whichever path suits your setup: ### Method A - From WordPress.org (recommended) 1. In your WordPress admin, go to **Plugins → Add New** 2. In the search box, type **PayBridgeNP** 3. Find **PayBridgeNP for WooCommerce** and click **Install Now** 4. Click **Activate** That's it. WordPress will handle future auto-updates for you. ### Method B - ZIP upload Use this if your host blocks WordPress.org's plugin search, or if you need a specific version. 1. Download the latest release from [paybridgenp.com/integrations/woocommerce](https://paybridgenp.com/integrations/woocommerce) by clicking the **Download ZIP** button. You'll get a file named `paybridge-np-woocommerce.zip` (about 2 MB; PHP SDK bundled, no Composer needed). 2. In your WordPress admin, go to **Plugins → Add New → Upload Plugin** 3. Click **Choose File** and select the ZIP 4. Click **Install Now**, then **Activate Plugin** The direct download URL is `https://paybridgenp.com/downloads/paybridge-np-woocommerce.zip`. You can link to it from your deployment scripts or fetch it via `curl` / `wget` if you prefer. ### Method C - From source (developers) ```bash theme={null} cd wp-content/plugins git clone https://github.com/paybridgenp/woocommerce.git paybridge-np-woocommerce cd paybridge-np-woocommerce composer install --no-dev --optimize-autoloader ``` Then activate the plugin in **Plugins → Installed Plugins**. Source clones don't get auto-updates - pull manually when a new release tag ships. WooCommerce must be installed and active before you activate this plugin. The `Requires Plugins: woocommerce` header on this plugin means WordPress 6.5+ will block activation outright until WooCommerce is present. ## Step 2 - Enable and configure the gateway 1. Go to **WooCommerce → Settings → Payments** 2. You'll see **PayBridgeNP** in the list of available payment methods. Click **Manage** next to it 3. Fill in the settings: | Field | Value | | -------------------------- | -------------------------------------------------------------------------------------------------------------------- | | **Enable/Disable** | Check the box to enable the gateway | | **Title** | `PayBridgeNP` (or whatever you want customers to see at checkout, e.g. `Pay with eSewa, Khalti, or Fonepay`) | | **Description** | Shown below the title on the checkout page. Defaults to `Pay securely with eSewa, Khalti, and more.` - edit to taste | | **Secret Key** | Paste your `sk_live_…` or `sk_test_…` key from the PayBridgeNP dashboard | | **Webhook Signing Secret** | Paste your `whsec_…` webhook signing secret - see Step 4 below for how to get it | 4. Click **Save changes** ## Step 3 - Set up the webhook The webhook is **required**. Without it, orders will be created but never move past **On hold** - they'll sit there forever because WooCommerce is waiting for PayBridgeNP to confirm the payment server-to-server. 1. In your [PayBridgeNP dashboard](https://dashboard.paybridgenp.com), go to **Webhooks → Add endpoint** 2. Set the **URL** to your store's webhook endpoint: ``` https://yourstore.com/?wc-api=paybridge_webhook ``` Replace `yourstore.com` with your actual domain. The `?wc-api=paybridge_webhook` query string is how WooCommerce routes the request to our gateway handler - it works regardless of your permalink structure. 3. **Events to subscribe to:** * `payment.succeeded` * `payment.failed` * `payment.cancelled` 4. Click **Create endpoint** 5. PayBridgeNP shows you the signing secret (`whsec_…`) **exactly once**. Copy it immediately. 6. Go back to **WooCommerce → Settings → Payments → PayBridgeNP**, paste the signing secret into the **Webhook Signing Secret** field, and click **Save changes** ## Step 4 - Place a test order **The plugin refuses to process webhooks that aren't signed.** If the Webhook Signing Secret field is empty, any webhook delivery is rejected with a 400 response. This is deliberate - without signature verification, an attacker could forge a `payment.succeeded` event and get free orders. Configure the signing secret before you go live. Switch your WordPress admin to a non-admin browser (or use an incognito window) so you're seeing the storefront as a customer would. 1. Add any product to your cart 2. Proceed to checkout 3. At the payment step, pick **PayBridgeNP** 4. Click **Place order** ### What should happen 1. WooCommerce redirects you to the PayBridgeNP hosted checkout page 2. You pick a wallet (eSewa, Khalti, or Fonepay) and complete the payment 3. PayBridgeNP redirects you back to your store 4. You land on the **Order received** page - the order shows as **On hold** 5. Within a few seconds, the signed webhook arrives, and the order automatically moves to **Processing** (the normal WooCommerce state for a paid order awaiting fulfillment) If the order stays **On hold** longer than 30 seconds, the webhook didn't arrive - jump to [Troubleshooting](/integrations/woocommerce/troubleshooting#orders-stuck-in-on-hold). ### Sandbox test credentials If you used a `sk_test_…` key, use these built-in test credentials on the PayBridgeNP hosted checkout: | Provider | Credential | | ---------- | ---------------------------------------------------------------- | | **eSewa** | ID `9806800001`, password `Nepal@123`, MPIN `1122`, OTP `123456` | | **Khalti** | Phone `9800000001`, MPIN `1111`, OTP `987654` | These credentials cover the eSewa and Khalti test environments. Fonepay has no sandbox: enabling it on a sandbox project uses your real Fonepay account and moves real money within the [sandbox caps](/guides/sandbox-testing). ## Local development webhook testing PayBridgeNP needs a publicly reachable URL to deliver webhooks. If your WordPress is running on `http://localhost`, you'll need a tunnel tool to expose it: ```bash theme={null} ngrok http 80 ``` Then use the generated `https://xxx.ngrok.io/?wc-api=paybridge_webhook` as your webhook endpoint URL in the PayBridgeNP dashboard. Switch it back to your real domain before going live. ## Updating the plugin If you installed via **Method A** (WordPress.org), updates are automatic. WordPress will show an update notice on **Dashboard → Updates** within \~24 hours of each release; click **Update Now**. If you installed via **Method B** (ZIP upload): 1. Download the latest ZIP from [paybridgenp.com/integrations/woocommerce](https://paybridgenp.com/integrations/woocommerce) (or from the WP.org listing) 2. In WordPress admin, go to **Plugins → Add New → Upload Plugin** 3. Choose the new ZIP and click **Install Now** - WordPress will ask if you want to **Replace current with uploaded**. Say yes. If you installed via **Method C** (source): ```bash theme={null} cd wp-content/plugins/paybridge-np-woocommerce git pull composer install --no-dev --optimize-autoloader ``` Your settings (API key, webhook secret, gateway title, etc.) persist across updates because they're stored in WordPress's `wp_options` table, not in the plugin directory. ## What's next * [How it works](/integrations/woocommerce/how-it-works) - payment flow, order state transitions, webhook handling * [Troubleshooting](/integrations/woocommerce/troubleshooting) - common install and runtime issues # WooCommerce Source: https://docs.paybridgenp.com/integrations/woocommerce/overview Accept eSewa, Khalti, and Fonepay payments on your WordPress/WooCommerce store through PayBridgeNP. PayBridgeNP for WooCommerce is an official WordPress plugin that adds a **PayBridgeNP** payment method to your WooCommerce checkout. Customers pick it like any other gateway, get redirected to PayBridgeNP's hosted checkout, pay with eSewa / Khalti / Fonepay, and land back on your order confirmation page - the order flips to Processing automatically via a signed webhook. ## Who this is for * You run a **WooCommerce** store on self-hosted WordPress * You want to accept **eSewa**, **Khalti**, or **Fonepay** from Nepali customers * You already have (or can create) a [PayBridgeNP account](https://dashboard.paybridgenp.com/signup) with a `sk_live_…` or `sk_test_…` API key * Your store's currency is **NPR** ## What you get * **Classic checkout + Block checkout.** The plugin registers itself with both the legacy shortcode checkout and the modern WooCommerce Blocks checkout out of the box. Works whether your theme uses the old or the new checkout flow. * **HPOS compatible.** The plugin declares compatibility with WooCommerce's High-Performance Order Storage, so it runs cleanly on modern WC installs using custom order tables. * **Signed webhook verification.** Every incoming `payment.succeeded` / `payment.failed` / `payment.cancelled` event is verified against an HMAC-SHA256 signature before it moves any order - forged events are rejected with a 400 response. * **Sandbox mode.** A `sk_test_…` key uses the eSewa and Khalti test environments. Fonepay has no sandbox and moves real money within the documented test caps. * **Permalink-agnostic.** Uses WooCommerce's `wc-api` endpoints, which work whether pretty-permalinks are enabled or not. No `.htaccess` surgery required. * **GPL-2.0-or-later licensed.** Same license as WordPress and WooCommerce core. ## How it plugs in ``` ┌────────────────┐ 1. places order (picks PayBridgeNP) ┌───────────────┐ │ Customer │ ───────────────────────────────────────▶│ WooCommerce │ └────────────────┘ └──────┬────────┘ │ 2. process_payment() │ creates PayBridgeNP session ▼ ┌──────────────────┐ │ PayBridgeNP │ │ hosted checkout │ └────────┬─────────┘ │ 3. customer pays ▼ ┌────────────────┐ 5. thank-you page (order on-hold) ┌──────────────────┐ │ Customer │ ◀───────────────────────────────────── │ handle_return() │ └────────────────┘ └──────┬───────────┘ │ │ 4. signed webhook ▼ ┌──────────────────┐ │ handle_webhook()│ │ → processing │ └──────────────────┘ ``` **Two independent confirmation paths:** the customer's browser returns to your store immediately (marking the order **On hold**), and PayBridgeNP delivers a signed webhook server-to-server that marks the order **Processing** with the transaction id recorded. The webhook is authoritative - if the customer's browser never makes it back (closed tab, dead battery), the order still flips to Processing once the webhook arrives. ## Embedded checkout (optional) By default the plugin sends your customer to the PayBridgeNP checkout page. You can instead have payment open in an overlay on your own store, so the customer stays on your site. Turn it on under **WooCommerce > Settings > Payments > PayBridgeNP** with **Open payment in an overlay instead of redirecting**. It is off by default. ### Before it will work Two things have to be true, and if either is not, the plugin quietly falls back to the normal redirect. Your customer always gets a working checkout. 1. **Your plan includes embedded checkout.** Growth and above. 2. **Your store domain is registered.** Add it in the dashboard under **Project > Embed domains** for the mode you are using. This is what gives your store permission to display the checkout, and you can revoke it at any time by removing the domain. ### What your customer sees The overlay opens on the order payment page, after the order is created, so your stock is already reserved by WooCommerce and the total is final. | Payment method | What happens | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Fonepay | The QR appears inside the overlay and the payment completes there. Your customer never leaves your store. | | eSewa, Khalti | These open in a separate window, because both providers block their payment pages from being displayed inside another site. Your customer returns to your store afterwards. | This is why the overlay is most useful if your customers pay with Fonepay. For eSewa and Khalti the overlay chooses the method and then hands off, which is a smaller gain than it sounds. ### Two things that change * **Payment method tiles become a pre-selection.** With the overlay on, picking eSewa or Khalti on your checkout page selects it inside the overlay rather than sending the customer straight to that provider. * **If anything is missing, you get the redirect.** An unregistered domain, a plan without the feature, a blocked script, or JavaScript turned off all fall back to the normal hosted checkout rather than failing. ## What this plugin does NOT do * **No in-store refund button.** Like the Shopify integration, refunds must be processed from the [PayBridgeNP dashboard](https://dashboard.paybridgenp.com) - not from WordPress admin. The wallet provider owns the refund path, not WooCommerce. * **NPR only.** Your store's currency must be set to NPR. Multi-currency support is on the roadmap. * **No subscriptions / recurring billing** from within WooCommerce. One-off payments only. If you need recurring billing, use PayBridgeNP's [billing plans API](/api-reference/billing/plans/create-plan) directly from a custom integration. * **Published on WordPress.org.** Install from the official [WordPress.org plugin directory](https://wordpress.org/plugins/paybridgenp-for-woocommerce/) or use the ZIP from [paybridgenp.com/integrations/woocommerce](https://paybridgenp.com/integrations/woocommerce) when you need a manual upload path. ## Next steps * [Install the plugin](/integrations/woocommerce/install) - download the ZIP, upload it to WordPress, configure the gateway * [How it works](/integrations/woocommerce/how-it-works) - the payment flow, webhook handling, and order state transitions explained * [Troubleshooting](/integrations/woocommerce/troubleshooting) - common issues and how to fix them # WooCommerce Troubleshooting Source: https://docs.paybridgenp.com/integrations/woocommerce/troubleshooting Common issues with PayBridgeNP for WooCommerce and how to fix them. Most problems on this page boil down to one of three things: the webhook signing secret isn't set, your WordPress site isn't publicly reachable, or the PayBridgeNP API key is wrong. Work through the matching section below and you'll likely fix it in under 2 minutes. ## Orders stuck in "On hold" **The single most common issue.** Customers complete payment and land on your thank-you page, but the order never moves from **On hold** to **Processing**. **Most likely cause:** PayBridgeNP can't reach your webhook endpoint, OR you haven't configured the webhook signing secret in WooCommerce settings. ### Diagnostic checklist 1. **Is your store publicly reachable over HTTPS?** PayBridgeNP only delivers webhooks to public HTTPS URLs. If you're running WordPress on `localhost`, a LAN IP, or behind a VPN, webhooks won't arrive. ```bash theme={null} curl -I https://yourstore.com/?wc-api=paybridge_webhook ``` You should get a response (a 400 is fine - it means the endpoint is reachable but rejects non-signed requests). If you get a connection timeout, your store isn't reachable from the internet. 2. **Is the webhook URL in the PayBridgeNP dashboard correct?** Go to [PayBridgeNP dashboard → Webhooks](https://dashboard.paybridgenp.com) and click the endpoint for your store. The URL should be: ``` https://yourstore.com/?wc-api=paybridge_webhook ``` Pay attention to: * `https://` not `http://` * Correct domain (no typos, trailing slashes, or `www.` where it shouldn't be) * `?wc-api=paybridge_webhook` query string present * The endpoint is **enabled** (check the toggle in the dashboard) 3. **Is the subscription list complete?** The webhook must subscribe to at least `payment.succeeded`. Recommended: all of `payment.succeeded`, `payment.failed`, `payment.cancelled`. 4. **Is the Webhook Signing Secret configured in WooCommerce?** Go to **WooCommerce → Settings → Payments → PayBridgeNP** and verify the **Webhook Signing Secret** field has a value starting with `whsec_`. If it's empty, every incoming webhook is rejected with HTTP 400 - this is deliberate, but means orders will never move past On hold. 5. **Check PayBridgeNP's delivery log.** In the PayBridgeNP dashboard, click the webhook endpoint → **Deliveries** tab. You'll see every event fired, the HTTP response code, and the response body. If you see `400 Invalid signature`, your signing secret doesn't match (copy-paste error? extra whitespace?). If you see `400 Webhook signing secret not configured`, set the secret in WooCommerce settings. If you see `200 OK`, the webhook was accepted and the plugin will have already moved the order. 6. **Check your PHP error log.** The plugin logs `[PayBridgeNP] Webhook received but no signing secret is configured.` via `error_log()` when a webhook arrives and the secret field is empty. Look for this in `/wp-content/debug.log` (if `WP_DEBUG_LOG` is enabled) or your server's PHP error log. ### Quick fix If you just set up the webhook and it's the first time through: 1. Make sure your store is publicly reachable (rule out localhost, staging sites behind HTTP auth, firewall-blocked subnets) 2. Copy the signing secret from PayBridgeNP → paste into WooCommerce settings → Save 3. In PayBridgeNP → Webhooks → your endpoint → click **Send test event** (or retry the failed delivery) 4. The order should flip to Processing within seconds ## Customer sees "Payment error: Invalid API key" on checkout **Cause:** the `Secret Key` field in WooCommerce settings has a typo, trailing whitespace, or is using a key from the wrong environment. **Fix:** 1. Go to the [PayBridgeNP dashboard](https://dashboard.paybridgenp.com) → Settings → API Keys 2. Copy the key directly from the dashboard (don't retype it) 3. Paste into **WooCommerce → Settings → Payments → PayBridgeNP → Secret Key** 4. Make sure you copied the whole string including the `sk_live_` or `sk_test_` prefix 5. Save and try again **Live vs sandbox mismatch:** the key prefix determines the mode. A `sk_test_…` key always runs in sandbox; a `sk_live_…` key always runs live. Make sure the key you pasted matches the environment you intend. ## Customer sees "PayBridgeNP: the PHP SDK is missing" **Cause:** You installed the plugin by copying source files (git clone or unzipping without running `composer install`), and the `vendor/` directory is missing. **Fix:** * **If you installed from the official ZIP** - re-download from [paybridgenp.com/integrations/woocommerce](https://paybridgenp.com/integrations/woocommerce). The official ZIP includes `vendor/` pre-built. If it's still missing, your WordPress upload must have stripped subdirectories - check your hosting provider's upload limits. * **If you installed from source** - inside the plugin directory, run: ```bash theme={null} composer install --no-dev --optimize-autoloader ``` This generates the `vendor/` directory with the PayBridgeNP PHP SDK. ## Webhook endpoint returns 400 "Webhook signing secret not configured" **Cause:** Merchant-side - you set up the webhook endpoint in PayBridgeNP dashboard but forgot to paste the signing secret into WooCommerce. **Fix:** Go to **WooCommerce → Settings → Payments → PayBridgeNP**, paste the `whsec_…` secret from the PayBridgeNP dashboard into the **Webhook Signing Secret** field, and save. The plugin refuses unsigned webhooks on purpose - without signature verification, an attacker could forge a `payment.succeeded` event and get free orders. There's no way to disable this check from the settings page. ## Webhook endpoint returns 400 "Invalid signature" **Cause:** The signing secret in WooCommerce settings doesn't match what PayBridgeNP is signing with. **Most common sub-causes:** * You copied a secret from the wrong webhook endpoint in the PayBridgeNP dashboard (if you have multiple endpoints) * Extra whitespace at the start or end of the pasted secret * You rotated the secret in the PayBridgeNP dashboard but didn't update the WooCommerce setting **Fix:** 1. Go to the PayBridgeNP dashboard → Webhooks → your endpoint 2. Look at the **Signing Secret** - note that PayBridgeNP only shows it once on creation, so if you've lost it, click **Rotate secret** to generate a new one 3. Paste the fresh secret into **WooCommerce → Settings → Payments → PayBridgeNP → Webhook Signing Secret** 4. Save and retry the failed delivery from the PayBridgeNP dashboard ## Customer completes payment but WooCommerce says "Payment was cancelled" **Cause:** Either the customer clicked cancel on PayBridgeNP's hosted checkout, OR the return URL was tampered with and has `&cancelled=1` appended. **Fix:** Check the order notes in WooCommerce admin for the reason. If the customer really cancelled, the order is correctly set to `cancelled`. If you see this for orders that should have succeeded, check your PayBridgeNP dashboard's delivery log to see what event actually fired. ## Block checkout not showing PayBridgeNP **Cause:** Your theme or a caching plugin is loading a stale JavaScript bundle. **Fix:** 1. Flush any object cache (Redis, Memcached, WP Rocket, W3 Total Cache, etc.) 2. Clear your browser cache for the site 3. Verify in **WooCommerce → Settings → Payments** that the gateway is still **Enabled** 4. Open your checkout page in an incognito window - if PayBridgeNP shows there, the issue is browser/cache, not the plugin If it still doesn't show, check your browser's DevTools console for JavaScript errors when the Blocks checkout loads. A script loading error (404 on `assets/js/paybridge-block.js`) usually means the plugin wasn't fully uploaded - re-upload the ZIP. ## "This plugin doesn't work with your version of WordPress" **Cause:** Your WordPress is older than 5.8 or your PHP is older than 7.4. **Fix:** Upgrade WordPress + PHP. Both 5.8 and 7.4 are years out of support - you'd be running an insecure stack regardless of this plugin. ## HPOS compatibility warning in WooCommerce admin **Symptom:** WooCommerce admin shows "This plugin is incompatible with High-Performance Order Storage." **Cause:** You're running an older version of the plugin (pre-1.0.0). **Fix:** Update to the latest version. The current plugin declares HPOS compatibility on boot and is fully tested against custom order tables. ## Still stuck? If none of the above describes your issue, [contact PayBridgeNP support](https://paybridgenp.com/contact) with: * Your store URL * The WooCommerce order number * The PayBridgeNP session id (visible in the order notes) * The approximate time the issue occurred (with timezone) * A copy of the delivery log entry from the PayBridgeNP dashboard, if relevant We'll pull our side of the delivery log and get back to you within one business day. # How PayBridgeNP works Source: https://docs.paybridgenp.com/introduction Learn how checkout sessions, provider settlement, API keys, sandbox testing, and payment confirmation fit together. PayBridgeNP gives your server one checkout API for eSewa, Khalti, and Fonepay. It handles each provider's request format, callback, and verification flow while you keep your own provider accounts. Payments settle directly into your eSewa, Khalti, or Fonepay merchant account. PayBridgeNP does not hold or settle your funds. ## Start with your goal * **Run a sandbox payment:** follow the [Quickstart](/quickstart). * **Build a custom flow:** create a session with the [Checkout API](/api-reference/checkout/create-session). * **Connect an existing store:** use the [Shopify](/integrations/shopify/overview), [WooCommerce](/integrations/woocommerce/overview), or [WHMCS](/integrations/whmcs/overview) integration. ## Payment flow
Sequence from checkout creation through provider verification and the signed webhook sent to your server
Your server creates the checkout and sends the buyer to its URL. PayBridgeNP handles the provider handoff; your server fulfills only after verifying the signed webhook and matching the payment's status, amount, and currency. ## Key concepts | Concept | What it means | | ---------- | ---------------------------------------------------------------------------------------- | | Amounts | Send integer paisa. `10000` represents NPR 100.00. | | Settlement | Funds move to your provider merchant account, not to PayBridgeNP. | | Projects | Each project has isolated API keys and provider credentials. | | API keys | Use `sk_test_` for sandbox and `sk_live_` for live payments. A key's mode cannot change. | | Sandbox | eSewa and Khalti use test environments. Fonepay uses real money within test caps. | ## Supported providers | Provider | Sandbox | Live | | -------- | ---------------------- | ---- | | eSewa | ✓ | ✓ | | Khalti | ✓ | ✓ | | Fonepay | ✓ (real money, capped) | ✓ | *** ## Get started Create a sandbox project in the [PayBridgeNP Dashboard](https://dashboard.paybridgenp.com), then follow the [Quickstart](/quickstart). # Quickstart Source: https://docs.paybridgenp.com/quickstart Accept your first payment in under 5 minutes. Create a hosted checkout, redirect the customer, and confirm the payment on your server. Keep your order amount in your database. The browser should send an order ID, not the amount to charge. ## 1. Get your API keys Sign up at [dashboard.paybridgenp.com](https://dashboard.paybridgenp.com) and copy your API key from **Settings → API Keys**. * Sandbox keys start with `sk_test_` - use these for testing * Live keys start with `sk_live_` - use these for real payments Never expose your API key in client-side code or commit it to version control. Use environment variables. ## 2. Install the SDK ```bash npm theme={null} npm install @paybridge-np/sdk ``` ```bash yarn theme={null} yarn add @paybridge-np/sdk ``` ```bash bun theme={null} bun add @paybridge-np/sdk ``` ## 3. Create a checkout session Call this from your server when a customer is ready to pay. ```typescript TypeScript (server) theme={null} import { PayBridgeNP } from "@paybridge-np/sdk"; const paybridgenp = new PayBridgeNP({ apiKey: process.env.PAYBRIDGENP_API_KEY!, // sk_test_... or sk_live_... }); export async function startCheckout(orderId: string) { const order = await loadOrderFromDatabase(orderId); if (!order) throw new Error("Order not found"); const session = await paybridgenp.checkout.create({ amount: order.amountPaisa, currency: order.currency, returnUrl: "https://yoursite.com/payment/return", cancelUrl: "https://yoursite.com/cart", metadata: { orderId: order.id }, }, `checkout-${order.id}`); return Response.redirect(session.checkout_url, 303); } ``` ```javascript JavaScript (fetch) theme={null} const res = await fetch("https://api.paybridgenp.com/v1/checkout", { method: "POST", headers: { "Authorization": `Bearer ${process.env.PAYBRIDGENP_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ amount: 10000, // load this from your order on the server returnUrl: "https://yoursite.com/success", cancelUrl: "https://yoursite.com/cart", metadata: { orderId: "ORD-001" }, }), }); const session = await res.json(); // Redirect customer to session.checkout_url ``` The response looks like: ```json theme={null} { "id": "cs_01j9x2k3m4n5p6q7r8s9t0u1v2", "checkout_url": "https://checkout.paybridgenp.com/checkout/cs_01j9x2k3m...", "flow": "hosted", "provider": null, "expires_at": "2026-03-31T12:30:00.000Z" } ``` Redirect your customer to `checkout_url`. They'll see the PayBridgeNP hosted checkout page and can pay with any provider you've configured. **`cancelUrl` is optional.** If you omit it, cancellations fall back to your `returnUrl` with `?status=cancelled` appended - and the hosted picker hides its "Cancel" link. Set `cancelUrl` only if you want a dedicated cancel page and a visible Cancel link on the picker. ## 4. Handle the return redirect After the customer pays (or cancels), they're redirected to your `returnUrl` (or `cancelUrl` if set and the customer cancelled) with query parameters: ``` https://yoursite.com/success?session_id=cs_xxx&status=success&payment_id=pay_xxx ``` | Parameter | Value | | ------------ | ---------------------------------------------- | | `session_id` | The checkout session ID | | `status` | `success`, `failed`, `cancelled`, or `expired` | | `payment_id` | The payment ID (only on success) | `expired` means the customer never finished and the session timed out. Treat it the same as `failed` for fulfilment purposes: no money moved. Branch on `success` and treat everything else as not-paid, rather than listing the failure values, so a future status can never be read as a payment. Do not fulfill orders based solely on the redirect. The customer could manipulate query parameters. Always verify payment server-side using webhooks or `GET /v1/payments/:id`. ## 5. Verify payment server-side ```typescript TypeScript (webhook, recommended) theme={null} type PaymentSucceeded = { id: string; amount: number; currency: string; metadata?: { orderId?: string }; }; export async function POST(request: Request) { const body = await request.text(); const signature = request.headers.get("x-paybridgenp-signature"); let event; try { event = await PayBridgeNP.webhooks.constructEvent( body, signature, process.env.PAYBRIDGENP_WEBHOOK_SECRET!, ); } catch { return new Response("Invalid signature", { status: 400 }); } if (event.type === "payment.succeeded") { const payment = event.data; const order = await loadOrderFromDatabase(payment.metadata?.orderId); if ( !order || payment.amount !== order.amountPaisa || payment.currency !== order.currency ) { return new Response("Payment does not match order", { status: 409 }); } await fulfillOrderOnce(order.id, payment.id); } return Response.json({ received: true }); } ``` ```typescript TypeScript (API check) theme={null} const order = await loadOrderFromDatabase("ORD-001"); const payment = await paybridgenp.payments.retrieve("pay_xxx"); if ( payment.status === "success" && payment.amount === order.amountPaisa && payment.currency === order.currency ) { await fulfillOrderOnce(order.id, payment.id); } ``` Replace the example data-access helpers with your own database calls. Make fulfillment idempotent by storing the PayBridgeNP payment ID and ignoring a repeat you already processed. Only have the `session_id` from the redirect? Fetch the session - once it's paid, the response includes `paymentId`: ```bash theme={null} curl https://api.paybridgenp.com/v1/sessions/cs_xxx \ -H "Authorization: Bearer sk_live_your_key" # → { "id": "cs_xxx", "status": "success", "paymentId": "pay_xxx", ... } ``` See [Get a checkout session](/api-reference/checkout/get-session). ## 6. Set up a webhook endpoint Go to **Webhooks** in the [dashboard](https://dashboard.paybridgenp.com), click **Add endpoint**, enter your URL, and select the events you want to receive. Save the signing secret - it's shown only once. See the [webhook verification guide](/guides/webhook-verification) for full details on signature verification. *** ## Next steps Test the full flow with built-in test credentials before going live. Add your real eSewa, Khalti, and Fonepay credentials. Make retries safe - avoid duplicate payments on network failures. Set up recurring subscriptions and automated invoicing. # CLI Source: https://docs.paybridgenp.com/sdk/cli Official command-line tool for the PayBridgeNP payment gateway. ## Installation ```bash npm theme={null} npm install -g @paybridge-np/cli ``` ```bash bun theme={null} bun install -g @paybridge-np/cli ``` ```bash yarn theme={null} yarn global add @paybridge-np/cli ``` After installing, both `paybridgenp` and `paybridge-np` are available as aliases. ```bash theme={null} paybridgenp --version ``` *** ## Authentication ### `paybridgenp login` Authenticate with your API key. You can find your keys in the [dashboard](https://dashboard.paybridgenp.com) under **Settings → API Keys**. ```bash theme={null} paybridgenp login ``` The CLI prompts for your key interactively. For CI/scripts, pass it directly: ```bash theme={null} paybridgenp login --key sk_test_... ``` Or set the environment variable to skip login entirely: ```bash theme={null} export PAYBRIDGENP_API_KEY=sk_test_... ``` The `PAYBRIDGENP_API_KEY` environment variable always takes priority over the saved config. Config is stored at: * **macOS/Linux:** `~/.config/paybridgenp/config.json` * **Windows:** `%APPDATA%\paybridgenp\config.json` The file is written with `0600` permissions - readable only by you. *** ### `paybridgenp status` Show the current authentication state and API connection. ```bash theme={null} paybridgenp status ``` ``` PayBridgeNP CLI Key sk_test_abc1... Mode sandbox API https://api.paybridgenp.com Source config file (~/.config/paybridgenp/config.json) ``` *** ## Payments ### `paybridgenp payments list` List recent payments in a table. ```bash theme={null} paybridgenp payments list paybridgenp payments list --limit 50 paybridgenp payments list --watch # live-refresh every 5s ``` | Flag | Default | Description | | ------------- | ------- | ------------------------------- | | `--limit ` | `20` | Number of results (max 100) | | `--watch` | off | Poll and redraw every 5 seconds | *** ### `paybridgenp payments get ` Fetch a single payment with full detail. ```bash theme={null} paybridgenp payments get pay_abc123 ``` ``` Payment pay_abc123 Amount NPR 500.00 Status success Provider esewa Provider ref 1234567890 Session cs_abc123 Created 16 Apr 2026, 12:34 NPT Metadata { "order_id": "123" } ``` *** ## Webhooks ### `paybridgenp webhooks list` List all registered webhook endpoints for the current project. ```bash theme={null} paybridgenp webhooks list ``` *** ### `paybridgenp webhooks listen` Start a local HTTP server and expose it via an [ngrok](https://ngrok.com) tunnel. Use this during development to receive real webhook events on your machine. ```bash theme={null} paybridgenp webhooks listen paybridgenp webhooks listen --port 4242 --secret whsec_... ``` ``` Webhook listener ready Local http://localhost:4242 Public https://abc123.ngrok-free.app Add this URL to your PayBridgeNP dashboard → Webhooks → Add endpoint Listening for events... (Ctrl+C to stop) [12:34:56] payment.succeeded id: pay_abc123 amount: NPR 100.00 provider: esewa provider_ref: TX123456 ``` | Flag | Default | Description | | ---------------------- | ------- | -------------------------------------------- | | `--port ` | `4242` | Local port to listen on | | `--secret ` | none | Verify incoming HMAC signatures | | `--forward ` | none | Forward verified events to another local URL | ngrok must be installed. Install it with `brew install ngrok` or from [ngrok.com](https://ngrok.com/download). *** ### `paybridgenp webhooks test ` Send a fake signed event to any URL to test your webhook handler. ```bash theme={null} paybridgenp webhooks test https://yoursite.com/webhooks/paybridgenp paybridgenp webhooks test http://localhost:4242 --secret whsec_... --event payment.failed ``` | Flag | Default | Description | | ---------------------- | ------------------- | ------------------------------ | | `--secret ` | none | Sign the payload (recommended) | | `--event ` | `payment.succeeded` | Event type to send | | `--amount ` | `10000` | Amount in the payload | **Supported event types:** | Event | Description | | ------------------- | ---------------------- | | `payment.succeeded` | Successful payment | | `payment.failed` | Failed payment attempt | | `payment.cancelled` | Customer cancelled | | `payment.refunded` | Refund issued | *** ## Testing ### `paybridgenp test` Create a real sandbox checkout session and open it in the browser. Complete a test payment end-to-end without writing any code. ```bash theme={null} paybridgenp test paybridgenp test --amount 50000 # NPR 500.00 paybridgenp test --no-open # print URL only ``` ``` Checkout session created Session ID cs_test_abc123 Amount NPR 10.00 URL https://checkout.paybridgenp.com/checkout/cs_test_abc123 Sandbox test credentials eSewa merchant code EPAYTEST Khalti phone 9800000001 · MPIN 1111 · OTP 987654 ``` | Flag | Default | Description | | ------------------ | ------- | ------------------------------------- | | `--amount ` | `1000` | Amount in paisa (NPR 10.00) | | `--no-open` | off | Print URL without opening the browser | If you run `paybridgenp test` with a live key (`sk_live_...`), the CLI will warn you and require explicit confirmation before creating a real checkout. *** ## Scaffolding ### `paybridgenp init` Interactively scaffold a starter project with working checkout and webhook routes. ```bash theme={null} paybridgenp init # Non-interactive (for CI or scripts): paybridgenp init --name my-shop --framework nextjs ``` | Flag | Description | | ------------------------- | --------------------------- | | `--name ` | Project directory name | | `--framework ` | `nextjs`, `node`, or `bare` | **What gets created:** ``` my-shop/ app/ api/ checkout/route.ts ← POST /api/checkout webhook/route.ts ← POST /api/webhook .env ← pre-filled with your API key .env.example .gitignore ← .env added automatically package.json ``` ``` my-shop/ index.ts ← HTTP server with /checkout + /webhook routes .env .env.example .gitignore package.json ``` ``` my-shop/ index.ts ← minimal SDK usage example .env .env.example .gitignore package.json ``` *** ## Updates ### `paybridgenp update` Check if a newer version is available on npm. ```bash theme={null} paybridgenp update ``` ``` Checking for updates... Current v1.0.3 Latest v1.0.3 ✓ You're up to date $ npm install -g @paybridge-np/cli@latest ``` *** ## Global flags These flags work with every command: | Flag | Description | | ----------- | ---------------------------------------------------------- | | `--debug` | Print full error stack traces instead of friendly messages | | `--version` | Print the CLI version | | `--help` | Show help for any command | ```bash theme={null} paybridgenp payments list --debug paybridgenp webhooks listen --debug ``` *** ## Configuration reference | Source | Priority | How to set | | ----------------------------- | ----------- | ----------------------------------- | | `PAYBRIDGENP_API_KEY` env var | 1 (highest) | `export PAYBRIDGENP_API_KEY=sk_...` | | Config file | 2 | Set by `paybridgenp login` | # Mobile SDK (React Native) Source: https://docs.paybridgenp.com/sdk/mobile Official PayBridgeNP React Native payment sheet for Fonepay, eSewa, and Khalti inside iOS and Android apps. ## Requirements * React Native 0.73+ or Expo SDK 50+ * `react-native-webview` >= 13 * `react-native-safe-area-context` >= 4 Expo Go can preview the sheet and hosted eSewa/Khalti screens. Native QR sharing and reliable installed-app detection require the config plugin and a development build. Expo Go cannot apply native app-query configuration. ## Installation ```bash npm theme={null} npm install @paybridge-np/mobile-sdk react-native-webview react-native-safe-area-context ``` ```bash yarn theme={null} yarn add @paybridge-np/mobile-sdk react-native-webview react-native-safe-area-context ``` ```bash expo theme={null} npx expo install @paybridge-np/mobile-sdk react-native-webview react-native-safe-area-context ``` Add the config plugin, then rebuild the native app: ```json app.json theme={null} { "expo": { "scheme": "myapp", "plugins": ["@paybridge-np/mobile-sdk"] } } ``` ```bash theme={null} npx expo prebuild ``` Use your normal local or EAS development-build command after prebuild. eSewa's sandbox Intent app is currently an Android UAT APK. *** ## How it works Your backend creates an amount-bound session using your secret API key. Your app receives that short-lived session and uses a publishable key to present the server-approved methods. **Your secret API key never touches the device.** ``` Your backend → POST /v1/mobile/session (secret key) → { session_id, client_secret, methods } Mobile app → PaymentSheet (publishable key) → server-confirmed result ``` *** ## Payment sheet (recommended) The payment sheet asks your backend for one session and renders whatever payment methods PayBridgeNP resolved for that merchant. Your app never picks a provider and never builds a payment UI, so turning Fonepay on for a merchant is a dashboard change, not an app release. The payment sheet showing Bank App, Fonepay QR, eSewa and Khalti The payment sheet showing Fonepay QR, eSewa and Khalti ### 1. Your backend creates a session Session creation requires the `payments:write` scope when your secret key has an explicit scope list. Unrestricted secret keys continue to work. The app uses a publishable key and the session client secret for the device calls. Call `POST /v1/mobile/session` with your **secret** key and no `provider`: ```bash theme={null} curl -X POST https://api.paybridgenp.com/v1/mobile/session \ -H "Authorization: Bearer sk_live_..." \ -H "Idempotency-Key: order_123_mobile" \ -H "Content-Type: application/json" \ -d '{ "amount": 245000, "currency": "NPR", "customer": { "name": "Ram Bahadur", "email": "ram@example.com", "phone": "98XXXXXXXX" } }' ``` ```json theme={null} { "session_id": "cs_...", "client_secret": "mcs_...", "amount": 245000, "expires_at": "2026-08-16T12:15:00Z", "methods": [ { "id": "fonepay_qr", "provider": "fonepay", "label": "Fonepay QR" }, { "id": "bank_intent", "provider": "fonepay", "label": "Bank App", "banks": [ ... ] }, { "id": "esewa", "provider": "esewa", "label": "eSewa", "esewa_intent_eligible": true }, { "id": "khalti", "provider": "khalti", "label": "Khalti" } ] } ``` `methods` is computed server-side from the merchant's enabled providers, their mode, their plan and the amount. A method the merchant cannot actually use is never returned, so the sheet cannot show a dead option. The device puts Bank App before Fonepay QR when a supported bank app is installed. One installed bank opens directly; multiple installed banks appear in a compact picker. Eligible eSewa Intent opens only when the eSewa app is detected. Otherwise, the sheet keeps the buyer in eSewa's hosted checkout. Return `session_id` and `client_secret` to your app. **Do not send your secret key to the device.** Use one stable `Idempotency-Key` per merchant order. A retry after a timeout or app restart then returns the same session instead of creating a second payable session. ### 2. Your app presents the sheet ```tsx theme={null} import { usePaymentSheet, PaymentSheet } from "@paybridge-np/mobile-sdk"; export default function CheckoutScreen() { const { present, paymentSheetProps } = usePaymentSheet({ publishableKey: "pk_live_...", // Required for eSewa Intent. Configure the same scheme in app.json. returnUrl: "myapp://paybridge/return", // Use config: { baseUrl: "https://staging-api.example.com" } outside production. fetchSession: () => fetch("https://your-backend.com/api/mobile-session", { method: "POST" }) .then((r) => r.json()), onComplete: ({ status, sessionId }) => { if (status === "success") console.log("server reports paid", sessionId); }, onCancel: () => {}, onError: (err) => console.error(err), }); return ( <>