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":"routing_group_inactive"}.

Environments & testing

Omit environment or use "auto" to follow the centrally authorized lifecycle: Test while the site is in preview, and Live only after Go Live. Use "sandbox" or "production" only to require that exact environment. In sandbox, Stripe test cards such as 4242 4242 4242 4242 move no real money.

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" \
  -H "Idempotency-Key: your-order-1042" \
  -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"}.
A 1–200 character Idempotency-Key header is required. NxtPay durably pins that key to one site and environment before calling the processor. On 502 {"error":"payment_unconfirmed"} the charge may exist — retry with the same key and payload; quote correlationId to support if it persists. A changed payload or explicit environment returns 409 idempotency_conflict. 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.
environmentstringauto (default) | sandbox | production. Explicit values require that exact environment.
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 means no recurring-capable processor is available for a fresh key; 503 pinned_route_unavailable means retry the same key when its pinned site recovers; 502 subscription_unconfirmed means retry with the same key and payload. 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. Persist one unique Idempotency-Key per logical refund. Reuse it only when retrying the same payment, amount, and reason.

curl -X POST https://payments.nxtpay.cc/v1/refunds \
  -H "Authorization: Bearer npk_YOUR_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: refund-2026-08-10-abc" \
  -d '{ "whitesiteId": "32db38b1-...", "paymentId": "73571f33-...", "amountCents": 2500, "reason": "customer_request" }'

Response 200: { "id":"...", "status":"succeeded", "amountCents":2500, ... }. A processing response is still being confirmed, so retry with the same key. Errors: 400 invalid_refund_request_id, 400 refund_exceeds_remaining, 404 payment_not_found, 409 idempotency_conflict, 409 refund_in_progress, 422 refund_provider_failed, 502 upstream_error.

422 refund_provider_failed is definitive and replays with the same key. Use a new key only when starting a new refund attempt.
A 502 upstream_error can be ambiguous. The provider may have accepted the refund. Retry with the same Idempotency-Key; do not create a new key for that attempt. A later legitimate refund, even for the same amount, must use a new key.

Routing status

GET /v1/routing-status

Whether this API key’s Routing Group has an active Whitesite route with an applied Provider Account binding — use it to show/hide the option at checkout. Returns { "available": true, "whitesiteCount": 3, "routingGroupId": "...", ... }.

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 · refund.failed · 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
403routing_group_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
409idempotency_conflictThe key was reused with a different payload or explicit environment; keep each logical request immutable
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
503no_whitesite_available / routing_state_unavailable / pinned_route_unavailableNo eligible route for a fresh key, Brain could not durably reserve the route, or the already-pinned site is temporarily unavailable
Ambiguous 502s (payment_unconfirmed, subscription_unconfirmed) 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."