Payment Sessions
A payment session is a server-created, short-lived authorization that ties a single payment operation to a specific provider. Think of it as a signed intent: your server declares what should happen (charge $100, add a card, authorize a hold), AccruPay issues a provider-specific token representing that intent, and your frontend executes it — without ever touching your API secret.
Sessions are the boundary between your server and the payment provider. Everything observable about a payment flows through them.
Session lifecycle
A session moves through a fixed set of states:
| Status | Meaning |
|---|---|
PENDING | Created, waiting for the customer to interact |
ACTIVE | Customer has opened the checkout UI |
COMPLETED | The provider accepted the operation |
CANCELED | The session was explicitly canceled before completion |
DECLINED | The provider rejected the payment (insufficient funds, card blocked, etc.) |
EXPIRED | The provider's token TTL elapsed before the customer completed the flow |
ERROR | An unexpected fault occurred during processing |
UNKNOWN | The provider returned a state AccruPay could not map |
COMPLETED means the session finished — not that money moved. Use the transaction's status (from verify()) to determine the payment outcome.
Session expiry is controlled by the payment provider, not AccruPay. Expired sessions return status: EXPIRED on verify() — this is not an error; it means the customer took too long. Create a new session and ask the customer to retry.
SafeCharge session — approximately 20 minutes from creation. Configurable in your Nuvei dashboard.
PaymentIntent — 24 hours by default. SetupIntents (add-payment-method sessions) do not expire on Stripe's side.
Session TTL is controlled by the provider, not AccruPay. Consult your payment provider's documentation for the exact expiry window. Design your checkout to handle EXPIRED status gracefully and create a fresh session on retry.
Session types
AccruPay issues sessions for three distinct operations:
- Payment sessions — charge a customer immediately
- Add-payment-method sessions — tokenize a card or bank account for future use, without charging
- Authorization sessions — place a hold on funds without capturing; capture separately later
Each type goes through the same lifecycle and uses the same verify() call. The difference is in which SDK namespace you call at start time.
Key session fields
When you call start(), AccruPay returns a session object. These are the fields you'll work with most:
| Field | Description |
|---|---|
id | Your primary reference. Stable, unique, safe to store and pass between services. |
status | Current lifecycle state (see table above). |
kind | CLIENT — customer completes via React SDK. SERVER — your server drives the operation directly. |
token | Provider-specific bearer credential. The React SDK reads this internally — you do not call it directly. |
tokenExpiresAt | When the provider token expires. This governs EXPIRED transitions. |
payload | Opaque provider config blob. Consumed by the React SDK; treat as a black box. |
amount | Amount in minor units (BigInt). |
currency | ISO 4217 currency code. |
merchantInternalTransactionCode | Your own idempotency key for the transaction, supplied at start(). |
transactionProviderId | The provider this session is bound to. |
token and payload are provider-specific and may change format between providers. Never build logic that depends on their internal structure.
Why verify() is the source of truth
The React SDK fires an onSuccess or onError callback when the customer finishes interacting. Do not use those callbacks to determine payment outcome.
The frontend operates inside a browser. Browsers can be closed mid-flight, network connections can drop between provider confirmation and your callback handler, and — critically — callbacks can be forged. A malicious user can trigger onSuccess without an actual payment.
The correct pattern is always: server starts session → customer pays → server verifies.
// server.ts
import AccruPay, { TRANSACTION_PROVIDER, CURRENCY, COUNTRY_ISO_2 } from '@accrupay/node';
const sdk = new AccruPay({
apiSecret: process.env.ACCRUPAY_API_SECRET,
});
// Step 1: start the session on your server
export async function createPaymentSession() {
const session = await sdk.transactions.clientSessions.payments.start({
merchantTransactionProviderId: process.env.ACCRUPAY_PROVIDER_ID,
data: {
amount: 10000n, // $100.00 in cents
currency: CURRENCY.USD,
merchantInternalCustomerCode: 'customer-123',
merchantInternalTransactionCode: 'order-456',
billing: {
billingFirstName: 'Jane',
billingLastName: 'Smith',
billingEmail: 'jane@example.com',
billingAddressCountry: COUNTRY_ISO_2.US,
},
storePaymentMethod: false,
},
});
// Send only the id to your frontend — never send the token or API secret
return { sessionId: session.id };
}
// Step 2: after the customer submits, verify on your server
export async function verifyPaymentSession(sessionId: string) {
// verify() returns the TRANSACTION — not the session
const transaction = await sdk.transactions.clientSessions.payments.verify({
id: sessionId,
});
if (transaction.status === 'SUCCEEDED') {
// Safe to fulfill the order
await fulfillOrder(transaction.merchantInternalTransactionCode);
}
return transaction;
}
Never trust a frontend payment confirmation without a server-side verify() call. A missing verification step creates a double-charge risk on retry and allows malicious actors to trigger order fulfillment without paying.
Four ways to identify a session on verify()
verify() accepts exactly one of these identifiers:
| Identifier | When to use |
|---|---|
id | The default. You stored the session id when you called start(). |
token | When the provider sends a callback to your server with its own session token. |
providerCode | When the provider callback includes a provider-side transaction reference. |
merchantInternalTransactionCode | When you need to look up by your own order/transaction ID (the value you passed at start()). Useful for idempotency checks and reconciliation. |
Pass only one. Passing more than one is a validation error.
// Identify by your order ID — useful when a webhook arrives before you have the session id
const transaction = await sdk.transactions.clientSessions.payments.verify({
merchantInternalTransactionCode: 'order-456',
});
verify() returns a transaction, not a session
This is a common point of confusion. verify() resolves the session into the underlying transaction record. The returned object has TRANSACTION_STATUS values:
PENDING | EXPIRED | STARTED | SUCCEEDED | ERROR | FAILED | CANCELED | UNKNOWN | DECLINED
A session status: COMPLETED paired with a transaction status: SUCCEEDED means the payment went through. A session status: COMPLETED paired with status: DECLINED means the provider completed the operation but rejected the payment.
Common mistakes
Trusting the React SDK callback. The onSuccess handler fires when the provider UI reports success — before your server has confirmed anything. Always verify server-side.
Storing the session token. token is a short-lived, provider-specific credential. Store id instead; it is stable and safe to reference across your system.
Retrying verify() on EXPIRED. An expired session cannot be recovered. Create a new session.
Conflating session status with transaction status. COMPLETED is a session state. SUCCEEDED is a transaction state. Both must be checked.