p3p-server-sdk
v1.3.0
Published
Pine Labs Online P3P Server SDK - Pine Labs Online P3P server middleware (TypeScript)
Maintainers
Readme
Pine Labs Online P3P Server SDK
TypeScript SDK for Pine Labs Online P3P server integrations. It generates
signed HTTP 402 payment challenges, verifies client Payment credentials,
captures payment through P3P debit, and builds Payment-Receipt headers.
Install
npm install p3p-server-sdkRequires Node.js >=18 or another runtime with fetch, AbortSignal.timeout,
and standard Web APIs.
Quick Start
import {
Amount,
ChargeOptions,
P3PEnvironment,
PaymentGateway,
PaymentMethod,
PineLabsOnlineP3P,
} from "p3p-server-sdk";
const p3p = PineLabsOnlineP3P.create({
clientId: "server-client-id",
clientSecret: "server-client-secret",
merchantId: "merchant-id",
paymentGateway: PaymentGateway.PineLabsOnline,
availablePaymentMethods: [PaymentMethod.RESERVE_PAY, PaymentMethod.OTM, PaymentMethod.CARD, PaymentMethod.CREDIT_EMI],
realm: P3PEnvironment.SANDBOX,
env: P3PEnvironment.SANDBOX,
});
const challenge = await p3p.generateChallenge(
new ChargeOptions(new Amount(50000, "INR"), "/api/premium"),
);Payment Configuration
paymentGateway is mandatory and currently supports
PaymentGateway.PineLabsOnline. availablePaymentMethods is mandatory and
controls what the server advertises inside each 402 challenge:
const config = {
clientId: "...",
clientSecret: "...",
merchantId: "...",
paymentGateway: PaymentGateway.PineLabsOnline,
availablePaymentMethods: [PaymentMethod.RESERVE_PAY, PaymentMethod.OTM, PaymentMethod.CARD, PaymentMethod.CREDIT_EMI],
env: P3PEnvironment.SANDBOX,
};clientId, clientSecret, and env are mandatory. merchantId is optional
for backward compatibility and required when sending RAP requests.
The SDK exchanges client credentials internally and refreshes its cached bearer
token before expiry. Static accessToken and baseUrl config fields are no
longer supported.
The local challenge HMAC key is derived internally from clientSecret with a
stable SDK prefix, so there is no separate challenge-signing config field.
Environment defaults:
| Env | URL | Timeout | Retries | Initial retry delay |
|---|---|---:|---:|---:|
| P3PEnvironment.SANDBOX | https://pluraluat.v2.pinepg.in | 60000 ms | 2 | 300 ms |
| P3PEnvironment.PRODUCTION | https://api.pluralpay.in | 45000 ms | 2 | 200 ms |
The generated challenge includes:
request.availablePaymentMethods: ["RESERVE_PAY", "OTM", "CARD", "CREDIT_EMI"]
For offer-led Credit EMI, pass PaymentMethod.CREDIT_EMI to
createPreAuthorization. The SDK preserves it as
payment_method: "CREDIT_EMI" and serializes structured merchant metadata:
await p3p.createPreAuthorization({
paymentMethod: PaymentMethod.CREDIT_EMI,
mobileNumber,
amount: new Amount(orderAmountPaise, "INR"),
validityInDays: 7,
merchantMetadata: {
offer_data: selectedOfferData,
p3p_offer_required: "true",
},
});During verification, the server SDK rejects client credentials whose
payload.payment_method is not advertised by the signed challenge and server
config. paymentGateway is not emitted in the server challenge payload.
Generic Middleware Flow
import {
Amount,
ChargeOptions,
decidePayment,
} from "p3p-server-sdk";
const decision = await decidePayment({
credentialHeader: request.headers.get("P3P-Credential") ?? undefined,
config,
chargeOptions: new ChargeOptions(new Amount(50000, "INR"), "/api/premium"),
});
if (decision.action !== "proceed") {
return new Response(JSON.stringify(decision.problemDetails), {
status: decision.status,
headers: decision.headers,
});
}
const response = await handler(request);
response.headers.set("Payment-Receipt", decision.headers["Payment-Receipt"]);
return response;402 Flow
- A request without
P3P-Credential: Payment ...receives402withWWW-Authenticate: Payment <challenge>. - A retried request with a credential is decoded and HMAC verified.
- The SDK authenticates with
POST /api/auth/v1/token. - The SDK captures payment with
POST /mpp/v1/debit. - The protected handler proceeds and the response receives
Payment-Receipt.
If /mpp/v1/debit returns 202 Accepted, the SDK treats that as an
accepted-but-processing debit. It does not re-POST /mpp/v1/debit — Pine
Labs rejects a resubmit with the same Idempotency-Key (422). Instead the
SDK resolves the terminal status by polling the read-only endpoint
GET /mpp/v1/debit/{id}:
- polls up to
maxRetriestimes until the debit reaches a terminal status - respects
Retry-Afterfrom the202response when Pine Labs returns it - falls back to
initialRetryDelayMsotherwise - genuine transient failures on the initial POST (network errors,
HTTP 429, and5xx) are still retried by the SDK's request layer
If the poll budget is exhausted and the debit is still non-terminal, the
middleware returns 202 with idempotencyKey on the result and the protected
resource must stay withheld. Application code can later call
p3p.getDebitStatus(idempotencyKey) to reconcile.
The debit request body uses the current P3P contract:
typeis the selected payment method, for example"RESERVE_PAY".customer.merchant_customer_referenceis populated from the client credential.payment_amount.valueis numeric minor units.payment_tokenis the one-shot token from the client credential.challenge_idis the server challenge id from the verified client credential.Idempotency-Keyis sent as a header;Merchant-IDis not sent by the SDK.
Receipt payloads include paymentGateway and paymentMethod when that context
is available. The older receipt method field is not emitted.
Mandates And Tokens
Server-side mandate creation is available through POST /mpp/v1/pre-authorize:
const mandate = await p3p.createMandate({
customerReference: "customer-ref-123",
amount: new Amount(50000, "INR"),
validityInDays: 20,
paymentMethod: PaymentMethod.RESERVE_PAY,
callbackUrl: "https://merchant.example/payments/callback",
});callbackUrl is serialized as the top-level callback_url field. The
snake-case callback_url spelling is also accepted. The field is optional, so
existing integrations preserve their current request shape when it is omitted.
Card pre-authorization uses the same endpoint and returns the service contract shape directly:
const preAuthorization = await p3p.createPreAuthorization({
paymentMethod: PaymentMethod.CARD,
mobileNumber: "9876543210",
amount: new Amount(1000, "INR"),
callbackUrl: "https://merchant.example/payments/callback",
validityInDays: 7,
description: "Card pre-auth for order-123",
});
console.log(preAuthorization.payment_method_reference_id);
// `challenge_url` / `redirect_url` points at the hosted checkout where the
// customer completes 3DS / card authorization. Open it in an iframe or
// redirect the customer to it, then wait for the mandate to become ACTIVE
// before capturing.
console.log(preAuthorization.redirect_url ?? preAuthorization.challenge_url);End-to-End Card Payment
The full CARD flow uses payment_method_reference_id returned by
createPreAuthorization to link the eventual debit back to the customer's
authorized card:
// 1. Create a card pre-authorization (customer completes the hosted checkout).
const preAuth = await p3p.createPreAuthorization({
paymentMethod: PaymentMethod.CARD,
mobileNumber: "9876543210",
amount: new Amount(50000, "INR"),
validityInDays: 7,
});
// 2. Direct the customer to the checkout URL (iframe or redirect).
// On success the pre-authorization transitions to ACTIVE.
const checkoutUrl = preAuth.redirect_url ?? preAuth.challenge_url;
// 3. Poll the mandate until it becomes ACTIVE.
let mandate = await p3p.getMandate(preAuth.payment_method_reference_id);
while (mandate.payment_status !== "ACTIVE") {
await new Promise((resolve) => setTimeout(resolve, 2000));
mandate = await p3p.getMandate(preAuth.payment_method_reference_id);
}
// 4. Charge the card via the standard 402 flow. The Server SDK issues a
// Payment challenge and, once the Client SDK returns a Payment credential
// that carries a token bound to this pre-auth, calls POST /mpp/v1/debit
// with `payment_method_reference_id: preAuth.payment_method_reference_id`.RAP card flows
RAP remains a CARD payment method. Pass typed rapDetails; the SDK nests it
under payment_method_details.rap_details. MPP, not the SDK, creates the
flattened PL_RAP_* metadata. Configure merchantId so RAP requests include
the required Merchant-ID header.
import { RAPOperation } from "p3p-server-sdk";
const registration = await p3p.createPreAuthorization({
paymentMethod: PaymentMethod.CARD,
mobileNumber: "9876543210",
amount: new Amount(500000, "INR"),
validityInDays: 56,
rapDetails: {
operation: RAPOperation.AUTONOMOUS_REGISTRATION,
contractVersion: "1",
requestId: "rap-aut-reg-001",
intentId: "intent-001",
agentId: "PL_AGENT_0001",
agentName: "Pine Labs Shopping Agent",
consumerReference: "consumer-001",
intentConstraints: {
bindingType: "VERIFIED_NAMES",
boundVerifiedNames: "Vijay Sales,Croma",
quotedAmount: "149900",
tolerance: "500",
maxPerDrawMinor: "150000",
maxTotalMinor: "500000",
maxDraws: "12",
drawConfirmation: "AUTO",
validFrom: "2026-09-01T00:00:00+05:30",
validTill: "2026-10-19T23:59:59+05:30",
},
},
});
const execution = await p3p.capture({
token: paymentToken,
amount: new Amount(149900, "INR"),
paymentMethod: PaymentMethod.CARD,
mobileNumber: "9876543210",
rapDetails: {
operation: RAPOperation.AUTONOMOUS_EXECUTION,
contractVersion: "1",
requestId: "rap-aut-exec-001",
registrationId: registration.payment_method_reference_id,
},
});When RAP is present and no explicit idempotency key is supplied, the SDK uses
rapDetails.requestId. For executions it also defaults challenge_id and
payment_method_reference_id to registrationId. Authorised operations use
RAPOperation.AUTHORISED_REGISTRATION and
RAPOperation.AUTHORISED_EXECUTION with their typed cart shapes.
The optional RAPIntentConstraints.quotedAmount,
RAPIntentConstraints.tolerance, and RAPCartSubSection.currency properties
are serialized as quoted_amount, tolerance, and currency, respectively.
On the client side, the Client SDK creates the payment token with
paymentMethod: PaymentMethod.CARD — see the Client SDK README for the
matching runtime context.
The server SDK intentionally does not expose token creation. The client/customer
flow obtains a one-shot token and sends it back in the P3P-Credential: Payment
credential. The server SDK verifies that credential and then calls
POST /mpp/v1/debit.
The server SDK also exposes debit status lookup by idempotency key:
const latestDebit = await p3p.getDebitStatus("idem_key_123");This calls GET /mpp/v1/debit/{id} and returns the same debit payload family
as the original debit call, so application code can reconcile a pending payment
later without re-running the full paid request flow.
Grantex environments
Configure the environment once when initializing the server SDK:
import {
GRANTEX_SANDBOX_BASE_URL,
PineLabsOnlineP3P,
} from "p3p-server-sdk";
const p3p = PineLabsOnlineP3P.create({
// Pine Labs configuration omitted
grantex: {
enforceGrant: true,
agentId: process.env.GRANTEX_AGENT_DID!,
issuer: GRANTEX_SANDBOX_BASE_URL,
hosted: {
apiKey: process.env.GRANTEX_API_KEY!,
baseUrl: GRANTEX_SANDBOX_BASE_URL,
},
},
});Lower/UAT uses https://grantex.test.pinelabs.com; production uses
https://grantex.pinelabs.com and is the default. Do not append /dashboard.
The SDK derives the consent page as <baseUrl>/consent?req=<authRequestId>.
Development
npm install
npm run build
npm testLicense
MIT
