Payments API
Take payments from your software in three steps: create a payment link, send the customer to the hosted checkout, then confirm the result by polling the order or receiving a signed webhook. One API key, amounts in cents.
Authentication
Send your secret API key on every /v1 request — as a bearer token or in the X-API-Key header. Keep it server-side; never ship it in client code.
Authorization: Bearer npk_YOUR_KEY
# or
X-API-Key: npk_YOUR_KEY
401 {"error":"missing_api_key"}, 401 {"error":"invalid_api_key"}, or 403 {"error":"money_site_inactive"}.Environments & testing
Set environment on the create-link call. Use "sandbox" to route to a test processor — pay with Stripe test cards like 4242 4242 4242 4242 (any future expiry, any CVC); no real money moves. Use "production" (default) for live charges.
Quickstart
1. Create a payment link → 2. redirect the customer to paymentUrl → 3. confirm via polling or a webhook → 4. refund if needed.
curl -X POST https://payments.nxtpay.cc/v1/payment-links \
-H "Authorization: Bearer npk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"amountCents": 2500,
"currency": "USD",
"description": "Pro plan",
"successUrl": "https://your-site.com/thanks",
"cancelUrl": "https://your-site.com/cart",
"customer": { "email": "[email protected]", "fullName": "Avery Chen" },
"partnerReference": "your-order-1042",
"environment": "sandbox"
}'
const res = await fetch("https://payments.nxtpay.cc/v1/payment-links", {
method: "POST",
headers: { Authorization: "Bearer npk_YOUR_KEY", "Content-Type": "application/json" },
body: JSON.stringify({
amountCents: 2500, currency: "USD", description: "Pro plan",
successUrl: "https://your-site.com/thanks",
cancelUrl: "https://your-site.com/cart",
customer: { email: "[email protected]", fullName: "Avery Chen" },
partnerReference: "your-order-1042", environment: "sandbox",
}),
});
const link = await res.json();
// window.location = link.paymentUrl;
$ch = curl_init("https://payments.nxtpay.cc/v1/payment-links");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer npk_YOUR_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode([
"amountCents" => 2500, "currency" => "USD", "description" => "Pro plan",
"successUrl" => "https://your-site.com/thanks",
"cancelUrl" => "https://your-site.com/cart",
"customer" => ["email" => "[email protected]", "fullName" => "Avery Chen"],
"partnerReference" => "your-order-1042", "environment" => "sandbox",
]),
]);
$link = json_decode(curl_exec($ch), true);
// header("Location: " . $link["paymentUrl"]);
The response gives you orderId, statusToken, and routedVia.whitesiteId — store all three; you need them to poll or refund.
Create a payment link
Creates an order + hosted checkout session.
/v1 API currently supports USD payments; non-USD requests return 400 {"error":"unsupported_currency"}.Idempotency-Key header (any stable string, e.g. your order id) so a retry replays the same link instead of charging again. On 502 {"error":"payment_unconfirmed"} the charge may exist — retry with the same Idempotency-Key, never a fresh one; quote the response's correlationId to support if it persists. The one exception: 409 {"error":"checkout_unrecoverable_use_new_key"} means this checkout's route is permanently gone — start a fresh checkout with a new key. partnerReference does not deduplicate.| Field | Type | Notes | |
|---|---|---|---|
amountCents | integer | required | Minor units. 1 – 10,000,000 (max $100,000.00). |
currency | string | USD only, default USD. Non-USD returns 400 unsupported_currency. | |
successUrl | url | required | Return URL after success. |
cancelUrl | url | required | Return URL on cancel. |
customer | object | required | Needs email. Optional firstName,lastName,fullName,phone,shippingAddress,billingAddress. Whatever you send is pre-filled on the hosted checkout, so the customer doesn't re-enter it. |
description | string | ≤255 chars. | |
partnerReference | string | Your order id, echoed back for reconciliation — does not deduplicate (use Idempotency-Key). | |
referringUrl | url | The page the customer came from — stored on the order for your analytics. | |
metadata | object | ≤30 keys; for your reporting only — never sent to the processor. | |
environment | string | sandbox | production (default). |
email, fullName, phone, billingAddress, shippingAddress — and NxtPay pre-fills them on the hosted checkout so the buyer doesn't re-enter their information. (Card number and CVC are always entered on the secure checkout page — NxtPay never handles card data.)Response 200:
{
"orderId": "8b0e4f1a-2c3d-4e5f-9a1b-2c3d4e5f6a7b",
"orderNo": "ORD-202606-DW9PGM",
"paymentId": "73571f33-e959-4705-9ab2-04c8ba2712a9",
"paymentUrl": "https://checkout.stripe.com/c/pay/cs_test_...",
"expiresAt": "2026-06-17T11:38:14.000Z",
"provider": "stripe",
"statusToken": "eyJ...",
"routedVia": { "whitesiteId": "32db38b1-...", "whitesiteName": "Acme Store",
"routeId": "...", "attemptNumber": 1, "capOverride": false }
}
expiresAt may be null — some providers' hosted links have no fixed server-side expiry.
Create a subscription
Same flow as a payment link, but the customer's card is vaulted and billed every interval. Redirect them to paymentUrl; renewals arrive as subscription.renewed webhooks. Send an Idempotency-Key header so a retry replays instead of creating a second subscription.
| Field | Type | Notes | |
|---|---|---|---|
amountCents | integer | required | Charged each cycle. Minor units. |
interval | string | day | week | month (default) | year. | |
intervalCount | integer | Intervals between charges (e.g. 3 + month = quarterly). Default 1; the period is capped at one year (day ≤ 365, week ≤ 52, month ≤ 12, year = 1). | |
signupFeeCents | integer | One-time fee added to the first invoice, in cents. amountCents stays the recurring price. | |
trialDays | integer | Free-trial days before the first recurring charge (≤ 730). The immediate (first) charge is (trial ? 0 : amountCents) + signupFeeCents — i.e. with a trial only the signup fee is taken now; without a trial the first charge is the recurring amount plus the signup fee. Must be ≤ 10,000,000. | |
successUrl / cancelUrl | url | required | Return URLs. |
customer | object | required | Needs email. |
currency, description, partnerReference, metadata, environment | Same as a payment link. currency is USD only (non-USD returns 400 unsupported_currency). |
curl -X POST https://payments.nxtpay.cc/v1/subscriptions \
-H "Authorization: Bearer npk_YOUR_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: sub-2026-06-24-abc" \
-d '{ "amountCents": 2500, "interval": "month", "successUrl": "https://you/welcome",
"cancelUrl": "https://you/cancel", "customer": { "email": "[email protected]" } }'
Response 200:
{
"orderId": "8b0e4f1a-...", "orderNo": "ORD-202606-DW9PGM",
"paymentId": "73571f33-...", "paymentUrl": "https://checkout.stripe.com/c/pay/cs_...",
"expiresAt": "2026-06-24T11:38:14.000Z", "provider": "stripe", "statusToken": "eyJ...",
"mode": "subscription", "interval": "month",
"subscriptionHandle": "OWIwZTRmMWEuLi4=",
"routedVia": { "whitesiteId": "32db38b1-...", "attemptNumber": 1 }
}
subscriptionHandle — it is all you need to cancel or look the subscription up. intervalCount is echoed when greater than 1. A subscription stays on the processor that opened it, so renewals don't re-route. 503 no_whitesite_available = no recurring-capable processor right now; 502 subscription_unconfirmed = retry with the same Idempotency-Key (never a fresh one); a 409 checkout_unrecoverable_use_new_key is the exception — start over with a new key. Validation errors: 400 invalid_interval, invalid_interval_count, invalid_signup_fee, invalid_trial_days, immediate_charge_exceeds_max.Cancel a subscription
Stops future cycles. Cancellation is immediate — billing stops the moment the cancel succeeds; there is no cancel-at-period-end option. Already-settled charges are not refunded. Idempotent — replaying the cancel returns alreadyCanceled: true with the original canceledAt.
curl -X POST https://payments.nxtpay.cc/v1/subscriptions/cancel \
-H "Authorization: Bearer npk_YOUR_KEY" -H "Content-Type: application/json" \
-d '{ "subscriptionHandle": "OWIwZTRmMWEuLi4=" }'
Response 200: { "ok": true, "subscriptionId": "sub_...", "canceledAt": "2026-07-08T09:14:02.000Z" }. Errors: 400 invalid_subscription_handle, 400 not_a_subscription, 404 subscription_not_found, 409 subscription_not_active_yet, 502 subscription_cancel_failed (transient — safe to retry).
Get a subscription
Current state of a subscription by its handle — the pull-based fallback if a subscription.canceled webhook is missed.
curl "https://payments.nxtpay.cc/v1/subscriptions/OWIwZTRmMWEuLi4=" \
-H "Authorization: Bearer npk_YOUR_KEY"
Response 200:
{
"orderId": "8b0e4f1a-...", "orderNo": "ORD-202606-DW9PGM",
"subscriptionId": "sub_...", "status": "active",
"canceledAt": null, "cancelReason": null,
"interval": "month", "intervalCount": 1,
"amountCents": 2500, "currency": "USD",
"trialDays": null, "signupFeeCents": null,
"createdAt": "2026-06-24T11:08:14.000Z", "startedAt": "2026-06-24T11:15:32.000Z",
"lastRenewalAt": "2026-07-24T11:15:40.000Z", "renewalCount": 1,
"nextBillingEstimate": "2026-08-24T11:15:40.000Z",
"subscriptionHandle": "OWIwZTRmMWEuLi4="
}
| Field | Notes |
|---|---|
status | pending — the customer hasn't completed the first checkout · active — billing · canceled — terminated. |
canceledAt / cancelReason | null unless canceled. cancelReason is best-effort, e.g. cancellation_requested | payment_failed | payment_disputed. |
amountCents | The recurring per-cycle price (minor units) — not the first-invoice total. |
startedAt | When the first payment succeeded; null while pending. |
lastRenewalAt / renewalCount | The most recent renewal cycle and how many have billed. |
nextBillingEstimate | A computed estimate (last renewal + interval) — not a provider-authoritative billing date. null when no further cycle is expected. |
trialDays / signupFeeCents | Echoes of the create call; null when not set. |
subscriptionId | The processor subscription id; null while pending. |
Errors: 400 invalid_subscription_handle, 400 not_a_subscription, 404 subscription_not_found, 429 rate_limited (60/min per IP), 502 subscription_lookup_failed (transient — safe to retry).
Get order status
Poll after the customer returns, or as a fallback to webhooks.
curl "https://payments.nxtpay.cc/v1/orders/8b0e4f1a-...?token=eyJ...&whitesiteId=32db38b1-..." \
-H "Authorization: Bearer npk_YOUR_KEY"
{
"id": "8b0e4f1a-...", "orderNo": "ORD-202606-DW9PGM",
"status": "paid", "currency": "USD", "totalCents": 2500, "refundedTotalCents": 0,
"paidAt": "2026-06-17T11:15:32.000Z",
"payments": [ { "id": "73571f33-...", "status": "succeeded", "attemptNo": 1,
"amountCents": 2500, "failureCode": null, "failureMessage": null } ]
}
Refund a payment
Refund a succeeded payment, fully or partially. Idempotent per (paymentId, amountCents).
curl -X POST https://payments.nxtpay.cc/v1/refunds \
-H "Authorization: Bearer npk_YOUR_KEY" -H "Content-Type: application/json" \
-d '{ "whitesiteId": "32db38b1-...", "paymentId": "73571f33-...", "amountCents": 2500, "reason": "customer_request" }'
Response 201: { "id":"...", "status":"succeeded", "amountCents":2500, ... }. Errors: 400 whitesiteId_required / paymentId_required, 400 refund_exceeds_remaining, 404 payment_not_found, 502 upstream_error.
Routing status
Whether NxtPay can route a payment right now — use it to show/hide the option at checkout. Returns { "available": true, "whitesiteCount": 3, ... }.
Register a webhook URL
Self-register (or re-point) the HTTPS endpoint NxtPay delivers signed webhooks to — the WooCommerce plugin calls this on every settings save, and any integration can use it. NxtPay mints one fresh whsec_... signing secret, wires your URL across every processor in your routing pool, and returns the secret.
curl -X POST https://payments.nxtpay.cc/v1/webhook-endpoint \
-H "Authorization: Bearer npk_YOUR_KEY" -H "Content-Type: application/json" \
-d '{ "url": "https://your-site.com/nxtpay/webhook" }'
Response 200:
{
"ok": true,
"secret": "whsec_...",
"webhookUrl": "https://your-site.com/nxtpay/webhook",
"connected": [ { "whitesiteId": "32db38b1-...", "name": "Acme Store" } ],
"failed": []
}
secret — it is only shown in this response, and it is what verifies every X-Nxtpay-Signature. Idempotent, but each call rotates the secret, so always overwrite your stored value. Errors: 400 invalid_url / url_must_be_https, 409 no_active_providers, 502 all_providers_failed / registration_unavailable (transient — retry).Heartbeat
Best-effort periodic check-in (the WooCommerce plugin sends one on a schedule). All body fields — pluginVersion, wpVersion, siteUrl — are optional metadata. Always answers 200 with { "ok": true, "routingAvailable": true }; routingAvailable says whether any processor is currently active.
Status reference
Order status
pending · awaiting_payment · paid · partially_refunded · refunded · disputed · canceled · fulfilled · completed · failed
Payment status
pending · succeeded · failed · expired · canceled
Webhooks
Configure a webhook endpoint + event subscriptions in your NxtPay dashboard, or self-register one with POST /v1/webhook-endpoint. NxtPay POSTs signed JSON to your URL. Always verify the signature before trusting an event.
Events
order.created · order.paid · order.failed · order.refunded · order.disputed · payment.succeeded · payment.failed · payment.expired · refund.succeeded · dispute.created · dispute.updated · subscription.renewed · subscription.payment_failed · subscription.canceled
Payload & headers
POST your-endpoint
X-Nxtpay-Signature: t=1719236132,v1=<hmac_hex>
X-Nxtpay-Event: order.paid
X-Nxtpay-Delivery-Id: delivery_...
{ "id": "delivery_...", "type": "order.paid", "created_at": "2026-06-17T11:15:32.000Z",
"data": { "orderId": "...", "orderNo": "ORD-...", "totalCents": 2500, "currency": "USD", "paidAt": "..." } }
Subscription event payloads
| Event | data fields | Notes |
|---|---|---|
subscription.renewed | subscriptionId, orderId, orderNo, amountCents, currency, paidAt, partnerReference? | orderId/orderNo are a new order booked for the renewal cycle; partnerReference echoes the one from the original create call so you can link the renewal back to your subscription. An order.paid for the renewal order follows. |
subscription.payment_failed | subscriptionId, orderId, failureMessage, partnerReference? | orderId is the original subscription order. The processor keeps retrying the charge on its own schedule. |
subscription.canceled | subscriptionId, orderId, canceledAt, reason?, partnerReference? | No further cycles will bill; already-settled charges are not refunded. canceledAt is ISO-8601 (provider-authoritative when available). If you miss this event, GET /v1/subscriptions/{subscriptionHandle} shows status: "canceled". |
Verify the signature (HMAC-SHA256)
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const p = Object.fromEntries(header.split(",").map(kv => kv.split("=")));
const expected = crypto.createHmac("sha256", secret).update(p.t + "." + rawBody).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1));
const fresh = Math.abs(Date.now()/1000 - Number(p.t)) < 300; // reject old (replay)
return ok && fresh;
}
HMAC_SHA256(secret, t + "." + rawBody) and compare to v1 with a constant-time check. De-duplicate on X-Nxtpay-Delivery-Id. Respond with any 2xx to acknowledge. Retries: up to 7 attempts, exponential backoff (1m → 24h), 10s timeout each.Errors
Errors are JSON with a machine-readable error code:
{ "error": "positive_amountCents_required", "message": "optional", "detail": "optional" }
| Status | Code | Meaning |
|---|---|---|
| 400 | positive_amountCents_required | amountCents missing or ≤ 0 |
| 400 | invalid_currency | currency is not a 3-letter code |
| 400 | unsupported_currency | currency is not USD |
| 400 | successUrl_and_cancelUrl_required | Missing return URLs |
| 400 | customer_required | customer object missing |
| 400 | invalid_interval / invalid_interval_count / invalid_signup_fee / invalid_trial_days / immediate_charge_exceeds_max | Subscription field invalid (see Create a subscription) |
| 400 | payment_link_rejected / subscription_rejected | Checkout cleanly rejected upstream (upstreamStatus carries the status) |
| 400 | invalid_subscription_handle / not_a_subscription | Bad subscription handle (cancel or lookup) |
| 400 | token_required / whitesiteId_required / paymentId_required | Missing poll/refund parameter |
| 400 | invalid_url / url_must_be_https | Webhook URL missing/invalid or not HTTPS |
| 400 | refund_exceeds_remaining | Refund > unrefunded balance |
| 401 | invalid_api_key | Bad / missing key |
| 403 | money_site_inactive | Key disabled |
| 404 | payment_not_found / whitesite_not_found / subscription_not_found | Not found / not owned by your key |
| 409 | subscription_not_active_yet / no_active_providers | Cancel before first payment / nothing to register a webhook against |
| 409 | checkout_unrecoverable_use_new_key | This checkout's payment route is permanently unavailable — the one exception to "retry the same key": start a fresh checkout with a new Idempotency-Key |
| 429 | rate_limited | Too many requests (see retryAfter) |
| 502 | payment_unconfirmed / subscription_unconfirmed | The charge may exist — retry with the same Idempotency-Key; quote correlationId to support |
| 502 | upstream_error / subscription_cancel_failed / subscription_lookup_failed / all_providers_failed / registration_unavailable | Transient upstream failure — safe to retry |
| 502 / 503 | all_whitesites_failed / no_whitesite_available | No processor could take the payment |
payment_unconfirmed, subscription_unconfirmed, all_whitesites_failed) include a correlationId — an opaque id (also in the X-Request-Id response header) you can quote to NxtPay support.Rate limits
Per-minute sliding windows, per API key and per client IP. Over a limit returns 429 {"error":"rate_limited","retryAfter":N} with a Retry-After header — back off for that many seconds.
| Endpoint | Per API key | Per IP |
|---|---|---|
POST /v1/payment-links | 30/min | 5/min |
POST /v1/subscriptions | 30/min | 5/min |
POST /v1/subscriptions/cancel | — | 20/min |
GET /v1/subscriptions/{subscriptionHandle} | — | 60/min |
GET /v1/orders/{id} | 60/min | 30/min |
POST /v1/refunds | 10/min | 5/min |
WordPress / WooCommerce plugin
Prefer a drop-in? Install the NxtPay plugin and paste your API key — no code.
- Download: /plugin/download (zip)
- Version metadata: /plugin/version
Build with AI
This API ships machine-readable docs so coding assistants (and you) can integrate fast. Point your AI tool at these:
Tip: paste the contents of /llms-full.txt into your AI coding tool and ask it to "integrate the NxtPay Payments API to take a $25 payment and verify the webhook."