Skip to main content

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:

StatusMeaning
PENDINGCreated, waiting for the customer to interact
ACTIVECustomer has opened the checkout UI
COMPLETEDThe provider accepted the operation
CANCELEDThe session was explicitly canceled before completion
DECLINEDThe provider rejected the payment (insufficient funds, card blocked, etc.)
EXPIREDThe provider's token TTL elapsed before the customer completed the flow
ERRORAn unexpected fault occurred during processing
UNKNOWNThe 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.

warning

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.

Session TTL by provider
Nuvei

SafeCharge session — approximately 20 minutes from creation. Configurable in your Nuvei dashboard.

Stripe

PaymentIntent — 24 hours by default. SetupIntents (add-payment-method sessions) do not expire on Stripe's side.

Other providers

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:

FieldDescription
idYour primary reference. Stable, unique, safe to store and pass between services.
statusCurrent lifecycle state (see table above).
kindCLIENT — customer completes via React SDK. SERVER — your server drives the operation directly.
tokenProvider-specific bearer credential. The React SDK reads this internally — you do not call it directly.
tokenExpiresAtWhen the provider token expires. This governs EXPIRED transitions.
payloadOpaque provider config blob. Consumed by the React SDK; treat as a black box.
amountAmount in minor units (BigInt).
currencyISO 4217 currency code.
merchantInternalTransactionCodeYour own idempotency key for the transaction, supplied at start().
transactionProviderIdThe 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;
}
danger

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:

IdentifierWhen to use
idThe default. You stored the session id when you called start().
tokenWhen the provider sends a callback to your server with its own session token.
providerCodeWhen the provider callback includes a provider-side transaction reference.
merchantInternalTransactionCodeWhen 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.