top of page

Process a token on your server

Validate the order, submit a REST sale, and classify approved, declined and uncertain outcomes.

Establish the server boundary

Your server must authenticate the user, authorize the order, calculate the authoritative amount and select the correct merchant before contacting NMI. Never trust a browser-provided total or merchant private key. Store money in an exact internal representation such as integer minor units, then serialize the expected API amount. online Load the unpaid order and check its currency, total and merchant. Create or lock a payment-attempt record so concurrent requests cannot both submit. Validate that a fresh token was supplied. Submit the token with the order-derived amount; persist the gateway response and transaction ID before answering the client.

Minimal sandbox request

Sale authorizes and captures in one operation. Do not include raw card fields alongside the token. The reference declares a numeric amount; some NMI recipes show a decimal string. This example follows the numeric reference and should be verified with your chosen runtime and sandbox account. POST /api/v5/payments/sale HTTP/1.1 Host: sandbox.nmi.com Authorization: <sandbox-private-key> Content-Type: application/json { "amount": 10.00, "payment_details": { "payment_token": "<fresh-payment-token>" } }

A small server-only transport helper

This server-only transport helper accepts an already validated order amount and token. Authentication, order lookup, concurrency control and persistence belong in your application. It returns approved, declined or unknown. Unknown never grants permission to retry: preserve the attempt and reconcile first. Even a definitive decline requires a deliberate customer action and your application’s attempt controls before a new submission. export async function submitSandboxSale({ token, amount, privateKey }) { if (!privateKey || !token || !Number.isFinite(amount) || amount <= 0) { throw new Error('Invalid server payment configuration'); } // This helper never grants retry permission. const unknown = (httpStatus = null, transactionId = null) => ({ outcome: 'unknown', httpStatus, transactionId, requiresReconciliation: true, safeToRetry: false }); let response; try { response = await fetch('https://sandbox.nmi.com/api/v5/payments/sale', { method: 'POST', headers: { Authorization: privateKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ amount, payment_details: { payment_token: token } }), signal: AbortSignal.timeout(15000) }); } catch { return unknown(); } let data; try { data = await response.json(); } catch { return unknown(response.status); } if (!data || typeof data !== 'object' || Array.isArray(data)) { return unknown(response.status); } const id = data.id == null ? null : String(data.id); if (!response.ok) return unknown(response.status, id); const result = String(data.response ?? ''); if (result === '1' && id && id !== '0') { return { outcome: 'approved', httpStatus: response.status, transactionId: id, requiresReconciliation: false, safeToRetry: false }; } if (result === '2') { return { outcome: 'declined', httpStatus: response.status, transactionId: id, requiresReconciliation: false, safeToRetry: false }; } return unknown(response.status, id); }

Interpret both response layers

The transport status and gateway transaction result answer different questions. A well-formed response may contain a decline. A timeout may occur after the gateway accepted the transaction. Avoid retry middleware that automatically repeats POST requests. Outcome Action HTTP success and response "1" Persist transaction ID and approved order state HTTP success with nonapproval Keep unpaid; display a safe decline message Validation error with details Correct input mapping; do not mark paid Timeout, invalid JSON, connection loss or ambiguous server error Keep attempt uncertain and reconcile before another charge

Retain a useful payment record

Store the merchant/environment, internal order and attempt IDs, gateway transaction ID, amount, currency, timestamps and normalized state. Keep raw secrets and payment fields out of logs. Use the payment ID for lookup and follow-on operations. Do not borrow a different NMI product’s idempotency header contract without endpoint-specific evidence. Next: run the first-payment recipe

Join the community and get the latest biz news & tips

Thanks for submitting!

© 2025 Betterpay. All Rights Reserved.

bottom of page