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.

Base URL: https://payments.nxtpay.cc Auth: API key (npk_…) Amounts: integer 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
Auth failures return 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"
  }'

The response gives you orderId, statusToken, and routedVia.whitesiteId — store all three; you need them to poll or refund.

Create a payment link

POST /v1/payment-links

Creates an order + hosted checkout session.

USD only — the /v1 API currently supports USD payments; non-USD requests return 400 {"error":"unsupported_currency"}.
Send an 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.
FieldTypeNotes
amountCentsintegerrequiredMinor units. 1 – 10,000,000 (max $100,000.00).
currencystringUSD only, default USD. Non-USD returns 400 unsupported_currency.
successUrlurlrequiredReturn URL after success.
cancelUrlurlrequiredReturn URL on cancel.
customerobjectrequiredNeeds 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.
descriptionstring≤255 chars.
partnerReferencestringYour order id, echoed back for reconciliation — does not deduplicate (use Idempotency-Key).
referringUrlurlThe page the customer came from — stored on the order for your analytics.
metadataobject≤30 keys; for your reporting only — never sent to the processor.
environmentstringsandbox | production (default).
Send whatever customer details you already have — 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

POST /v1/subscriptions

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.

FieldTypeNotes
amountCentsintegerrequiredCharged each cycle. Minor units.
intervalstringday | week | month (default) | year.
intervalCountintegerIntervals between charges (e.g. 3 + month = quarterly). Default 1; the period is capped at one year (day ≤ 365, week ≤ 52, month ≤ 12, year = 1).
signupFeeCentsintegerOne-time fee added to the first invoice, in cents. amountCents stays the recurring price.
trialDaysintegerFree-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 / cancelUrlurlrequiredReturn URLs.
customerobjectrequiredNeeds email.
currency, description, partnerReference, metadata, environmentSame 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 }
}
Store 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

POST /v1/subscriptions/cancel

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

GET /v1/subscriptions/{subscriptionHandle}

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="
}
FieldNotes
statuspending — the customer hasn't completed the first checkout · active — billing · canceled — terminated.
canceledAt / cancelReasonnull unless canceled. cancelReason is best-effort, e.g. cancellation_requested | payment_failed | payment_disputed.
amountCentsThe recurring per-cycle price (minor units) — not the first-invoice total.
startedAtWhen the first payment succeeded; null while pending.
lastRenewalAt / renewalCountThe most recent renewal cycle and how many have billed.
nextBillingEstimateA computed estimate (last renewal + interval) — not a provider-authoritative billing date. null when no further cycle is expected.
trialDays / signupFeeCentsEchoes of the create call; null when not set.
subscriptionIdThe 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

GET /v1/orders/{id}?token={statusToken}&whitesiteId={whitesiteId}

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

POST /v1/refunds

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

GET /v1/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

POST /v1/webhook-endpoint

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": []
}
Store the returned 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

POST /v1/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

Eventdata fieldsNotes
subscription.renewedsubscriptionId, 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_failedsubscriptionId, orderId, failureMessage, partnerReference?orderId is the original subscription order. The processor keeps retrying the charge on its own schedule.
subscription.canceledsubscriptionId, 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;
}
Compute 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" }
StatusCodeMeaning
400positive_amountCents_requiredamountCents missing or ≤ 0
400invalid_currencycurrency is not a 3-letter code
400unsupported_currencycurrency is not USD
400successUrl_and_cancelUrl_requiredMissing return URLs
400customer_requiredcustomer object missing
400invalid_interval / invalid_interval_count / invalid_signup_fee / invalid_trial_days / immediate_charge_exceeds_maxSubscription field invalid (see Create a subscription)
400payment_link_rejected / subscription_rejectedCheckout cleanly rejected upstream (upstreamStatus carries the status)
400invalid_subscription_handle / not_a_subscriptionBad subscription handle (cancel or lookup)
400token_required / whitesiteId_required / paymentId_requiredMissing poll/refund parameter
400invalid_url / url_must_be_httpsWebhook URL missing/invalid or not HTTPS
400refund_exceeds_remainingRefund > unrefunded balance
401invalid_api_keyBad / missing key
403money_site_inactiveKey disabled
404payment_not_found / whitesite_not_found / subscription_not_foundNot found / not owned by your key
409subscription_not_active_yet / no_active_providersCancel before first payment / nothing to register a webhook against
409checkout_unrecoverable_use_new_keyThis checkout's payment route is permanently unavailable — the one exception to "retry the same key": start a fresh checkout with a new Idempotency-Key
429rate_limitedToo many requests (see retryAfter)
502payment_unconfirmed / subscription_unconfirmedThe charge may exist — retry with the same Idempotency-Key; quote correlationId to support
502upstream_error / subscription_cancel_failed / subscription_lookup_failed / all_providers_failed / registration_unavailableTransient upstream failure — safe to retry
502 / 503all_whitesites_failed / no_whitesite_availableNo processor could take the payment
Ambiguous 502s (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.

EndpointPer API keyPer IP
POST /v1/payment-links30/min5/min
POST /v1/subscriptions30/min5/min
POST /v1/subscriptions/cancel20/min
GET /v1/subscriptions/{subscriptionHandle}60/min
GET /v1/orders/{id}60/min30/min
POST /v1/refunds10/min5/min

WordPress / WooCommerce plugin

Prefer a drop-in? Install the NxtPay plugin and paste your API key — no code.

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