P3P Client and Server SDKs
Install and use Pine Labs P3P Client and Server SDKs for TypeScript and Python.
SDKs handle the protocol mechanics — challenges, tokens, retries, capture, and receipts — so your code stays focused on the business logic. Every HTTP interaction remains observable for debugging, but you never construct headers or manage token lifecycles manually.
📘 Security note: Do not instantiate
p3p-client-sdkin browser code when your integration usesclientSecretor other trusted merchant credentials. Run it only in a backend, server function, or trusted merchant service. Browser/front-end code should call your own backend endpoints.
Install SDK
# Client
npm i p3p-client-sdk
# Server
npm i p3p-server-sdk
Get TypeScript SDK from npm:
| Variant | Main import | What it does |
|---|---|---|
| Client | PineLabsOnlineClient from p3p-client-sdk | Requests paid resources, handles 402, creates one-time payment tokens, retries with P3P-Credential: Payment, and reads receipts. Use it only from a trusted environment when credentials are required. |
| Server | PineLabsOnlineP3P or decidePayment from p3p-server-sdk | Creates mandates, prices routes, returns challenges, verifies credentials, captures payment, and returns Payment-Receipt. |
Prerequisite
Complete these steps before adding Grantex to your integration:
Go to https://grantex.dev/ and sign in.
If you are a new user, create an account at https://grantex.dev/dashboard/signup.
Create an account in Grantex using your Org Name and Email ID. After account creation, you will get the API Key.
📘 Note: Save your API key — you won't see it again.
Add mpp:payment:initiate to the Agent. If you want bounded payments, add a spend-limit scope pattern such as mpp:payment:max_txn_paise:* to the Agent.
During customer consent, request a concrete cap such as mpp:payment:max_txn_paise:50000. In server-side requiredScopes, require only scopes that are actually present in the grant token, commonly ['mpp:payment:initiate'].
Use this API key and Agent ID when initializing the P3P SDK. Hosted Grantex authorization and code exchange use the raw Agent ID, for example ag_.... Grant verification may require DID form, for example did:grantex:ag_....
📘 Note: Need help with integration?
Reach out to our integration support team at pgintegration@pinelabs.com
Steps for P3P Payment Flow
- Grantex Authorization and Grant Storage
- Setup Customer Mandate
- Request Paid Resource
- Handle 402 Challenge and Retry With Payment Credential
- Paid Resource Handler Captures Payment
- Server Polls Debit Status and Troubleshoots
1. Grantex Authorization and Grant Storage
Use this backend-led flow for hosted Grantex: start consent after Buy Now, handle callback, exchange code, allocate budget, then persist the grant token server-side for paid API calls.
Important runtime details:
- Use the raw Grantex Agent ID, for example
ag_..., for hosted authorization and code exchange. - Add your own
stateparameter to the callback URL. Some hosted callbacks may not includeauthRequestId. - Allocate hosted Grantex budget in paise, matching P3P
Amount.value. Do not divide by 100. - Do not ask customers for grant tokens or grant IDs.
import { randomUUID } from "node:crypto";
// 1) Start consent after Buy Now
const state = randomUUID();
const redirectUri = new URL("https://merchant.example.com/grantex/callback");
redirectUri.searchParams.set("state", state);
const auth = await serverP3P.createGrantexAuthorization({
userId: customerId,
agentId: process.env.GRANTEX_AGENT_ID!, // raw ag_... hosted Agent ID
scopes: ["mpp:payment:initiate", "mpp:payment:max_txn_paise:50000"],
redirectUri: redirectUri.toString(),
expiresIn: "24h",
});
// Save state -> customer mapping. Also store authRequestId when present.
await store.save(state, {
authRequestId: auth.authRequestId,
customerId,
budgetPaise: 50000,
});
if (auth.authRequestId) {
await store.save(auth.authRequestId, {
state,
customerId,
budgetPaise: 50000,
});
}
// Redirect user to Grantex UI.
return Response.redirect(auth.consentUrl);
// 2) Callback after user authorizes
// GET /grantex/callback?code=...&state=...
const url = new URL(request.url);
const code = url.searchParams.get("code")!;
const callbackState = url.searchParams.get("state") ?? undefined;
const authRequestId = url.searchParams.get("authRequestId") ?? undefined;
const pending = await store.get(callbackState ?? authRequestId!);
if (!pending) {
throw new Error("Payment authorization expired");
}
// Exchange code -> grant token + grantId.
const exchanged = await serverP3P.exchangeGrantexCode({
code,
agentId: process.env.GRANTEX_AGENT_ID!, // raw ag_... hosted Agent ID
});
// Optional: allocate budget (NOT automatic).
// Use paise, matching P3P Amount.value.
await serverP3P.allocateGrantexBudget({
grantId: exchanged.grantId,
initialBudget: pending.budgetPaise,
currency: "INR",
});
// Persist grant for later paid API calls.
await grantStore.save(pending.customerId, {
grantToken: exchanged.grantToken,
grantId: exchanged.grantId,
scopes: exchanged.scopes,
expiresAt: exchanged.expiresAt,
});
return new Response("Grantex connected");
Then for paid requests, your app attaches the stored grantToken as X-Grantex-Token. In most merchant applications, the browser should not see this token; your backend can attach it while calling the paid route.
2. Setup Customer Mandate
For the current UPI ReservePay rail, the customer must have an active mandate before paid requests can complete. Create that mandate with the server SDK before requesting paid routes. Use the same 10-digit mobile number for mandate creation, token creation, and debit.
import {
Amount,
PaymentMethod,
PineLabsOnlineP3P
} from "p3p-server-sdk";
const p3p = PineLabsOnlineP3P.create(config);
const mobileNumber = "98******10";
const mandate = await p3p.createMandate({
customerReference: "customer-ref-123",
mobileNumber,
amount: new Amount(100000, "INR"),
validityInDays: 20,
paymentMethod: PaymentMethod.RESERVE_PAY
});
The amount uses paise. 100000 means Rs 1,000.
The createMandate response includes a deep_link field — a UPI intent URL that the customer must approve in their UPI app. Follow these steps:
- Extract deep_link — Read deep_link from the create mandate response.
- Generate a QR code — Use any QR library (for example,
qrcodein Python or JavaScript) to encode the deep_link into a scannable QR. - Display to customer — Show the QR code on your UI. The customer scans it with their UPI app to approve the mandate.
- Poll for activation — After the customer scans, poll the mandate status until it becomes
ACTIVE,APPROVED, orSUCCESS.
3. Request Paid Resource
In this step, your application simply requests the paid resource through the client SDK. The client SDK does not create the payment token before the first request. Token creation happens only if the server returns a P3P 402 challenge.
If your client SDK configuration uses clientSecret, this code must run in a backend or other trusted environment, not in browser code.
import {
P3PEnvironment,
PaymentMethod,
PineLabsOnlineClient
} from "p3p-client-sdk";
const client = PineLabsOnlineClient.create({
env: P3PEnvironment.SANDBOX,
clientId: process.env.PINELABS_CLIENT_ID!,
clientSecret: process.env.PINELABS_CLIENT_SECRET!
});
const response = await client.get(
"https://server.example.com/api/weather",
{},
{
mobileNumber: "98******10",
paymentMethod: PaymentMethod.RESERVE_PAY,
grantexToken: user_grant_token,
}
);
Pass PaymentMethod.OTM as paymentMethod in the runtime context when the server challenge advertises OTM.
4. Handle 402 Challenge and Retry With Payment Credential
This step is automatic when you use the client SDK request methods.
- The client SDK sends the first request to the paid route.
- The server route returns a
402challenge inWWW-Authenticatewhen noP3P-Credentialis present. - The client SDK decodes that challenge.
- The client SDK creates a one-time payment token using the configured Pine Labs credentials and runtime context.
- The client SDK builds
P3P-Credential: Payment <payload>. - The client SDK retries the same HTTP request with
P3P-CredentialandX-Grantex-Token. - The server receives the retry, verifies the credential, captures payment, and returns the protected response plus
Payment-Receiptwhen successful.
Your application code should treat this as one paid-resource request. The SDK owns the challenge, token, credential, retry, and receipt mechanics.
5. Paid Resource Handler Captures Payment
Define this handler on your server for each route that requires payment. decidePayment / decide_payment handles all server-side states: 402 challenge, credential verification, Grantex grant verification, debit capture, pending status, and receipt generation.
Before capture, make sure the customer has an active mandate for the same mobile number and enough mandate amount.
import {
Amount,
ChargeOptions,
P3PEnvironment,
PaymentGateway,
PaymentMethod,
decidePayment
} from "p3p-server-sdk";
const rawGrantexAgentId = process.env.GRANTEX_AGENT_ID!;
const grantexVerifierAgentId = process.env.GRANTEX_AGENT_DID
?? (rawGrantexAgentId.startsWith("did:")
? rawGrantexAgentId
: `did:grantex:${rawGrantexAgentId}`);
const decision = await decidePayment({
credentialHeader: request.headers.get("P3P-Credential") ?? undefined,
grantexTokenHeader: request.headers.get("X-Grantex-Token") ?? undefined,
config: {
clientId: process.env.PINELABS_CLIENT_ID!,
clientSecret: process.env.PINELABS_CLIENT_SECRET!,
paymentGateway: PaymentGateway.PineLabsOnline,
availablePaymentMethods: [PaymentMethod.RESERVE_PAY, PaymentMethod.OTM],
env: P3PEnvironment.SANDBOX,
realm: P3PEnvironment.SANDBOX,
grantex: {
enforceGrant: true,
agentId: grantexVerifierAgentId,
issuer: process.env.GRANTEX_ISSUER ?? "https://grantex.dev",
requiredScopes: ["mpp:payment:initiate"],
hosted: {
apiKey: process.env.GRANTEX_API_KEY!,
baseUrl: process.env.GRANTEX_BASE_URL ?? "https://api.grantex.dev"
}
}
},
chargeOptions: new ChargeOptions(new Amount(10000, "INR"), "/api/weather")
});
if (decision.action !== "proceed") {
return Response.json(
decision.problemDetails ?? { status: decision.status, title: "Payment required" },
{ status: decision.status, headers: decision.headers }
);
}
return Response.json(
{ forecast: "Clear", paid: true, capture: decision.captureResult },
{ headers: decision.headers }
);
6. Server Polls Debit Status and Troubleshoots
When decision.action === "pending" (202), store the idempotency key from decision.problemDetails.idempotencyKey and poll until the debit reaches a terminal state.
// idempotencyKey is available on the pending decision from Step 5:
// const idempotencyKey = decision.problemDetails?.idempotencyKey;
const latestDebit = await p3p.getDebitStatus("idem_key_123");
if (latestDebit.status === "SUCCESS") {
// Mark the order paid.
}
if (latestDebit.status === "FAILED") {
// Mark the order failed and show diagnostics.
}
If the debit reaches a terminal FAILED status, inspect the debit payload from getDebitStatus. Also verify:
- the mandate status is
ACTIVE,APPROVED, orSUCCESS - the mobile number used for mandate, token creation, and debit is the same 10-digit number
- the mandate balance is sufficient
- the
payment_method_reference_idin the debit matches the active ReservePay mandate - the sandbox mobile/test data supports successful debit
Some sandbox failures may return a generic failure_reason: "FAILED". Share the Pine Labs order_id, merchant debit reference/idempotency key, mandate reference, and mobile number with integration support.
Fetch ReservePay Balance
Mandate balance lookup is a server SDK call and currently supports PaymentMethod.RESERVE_PAY.
import {
PaymentMethod,
PineLabsOnlineP3P
} from "p3p-server-sdk";
const p3p = PineLabsOnlineP3P.create(config);
const balance = await p3p.getMandateBalance({
authorizationId: "auth_123",
phoneNumber: "98******10",
paymentMethod: PaymentMethod.RESERVE_PAY
});
const blocked = balance.amount?.value ?? 0;
const debited = balance.balance_details?.amount_debited.value ?? 0;
const remaining = balance.balance_details?.amount_remaining.value ?? 0;
SDK Methods Reference
| Helper | Use |
|---|---|
client.get/post/put/delete/patch/request | Request paid server endpoints with automatic 402 handling, token creation, credential retry, and receipt parsing. |
client.methods.createToken / client.methods.create_token | Create a one-time payment token when manually handling a challenge. Most integrations use automatic request handling instead. |
PineLabsOnlineP3P.create(config).createMandate(...) / PineLabsOnlineP3P.create(config).create_mandate(...) | Create UPI ReservePay payment capacity for a customer. |
PineLabsOnlineP3P.create(config).getMandateBalance(...) / PineLabsOnlineP3P.create(config).get_mandate_balance(...) | Fetch ReservePay mandate balance from GET /mpp/v1/balance. |
PineLabsOnlineP3P.create(config).getDebitStatus(...) / PineLabsOnlineP3P.create(config).get_debit_status(...) | Get debit status by idempotency key from GET /mpp/v1/debit/{id}. |
decidePayment / decide_payment | Framework-agnostic route helper for challenge, verification, capture, pending status, and receipts. |
PineLabsOnlineP3P.create(config) | Create a server SDK instance for lower-level control. |
PineLabsOnlineP3P.create(config).createGrantexAuthorization(...) / .create_grantex_authorization(...) | Start Grantex consent — returns consentUrl and usually authRequestId. |
PineLabsOnlineP3P.create(config).exchangeGrantexCode(...) / .exchange_grantex_code(...) | Exchange callback code for a grantToken and grantId. |
PineLabsOnlineP3P.create(config).allocateGrantexBudget(...) / .allocate_grantex_budget(...) | Set the initial spend budget for a grant using the same minor-unit amount as Amount.value, for example paise for INR. |
PineLabsOnlineP3P.create(config).debitGrantexBudget(...) / .debit_grantex_budget(...) | Debit grant budget using the same minor-unit amount as Amount.value, for example paise for INR. |
Headers
| Header | Direction | Meaning |
|---|---|---|
WWW-Authenticate | Server to client | Carries the unpaid request challenge with amount, resource, and expiry. |
P3P-Credential | Client to server | Carries the payment credential on retry. |
Payment-Receipt | Server to client | Carries receipt details after capture succeeds. |
X-Grantex-Token | Client to server | Carries the Grantex grant JWT. Required on every request when enforceGrant: true is set in the server config. |
Protocol Objects
| Object | Purpose |
|---|---|
| Mandate | Customer-approved payment capacity for the current UPI ReservePay rail. |
| Challenge | Server-signed payment requirement returned in WWW-Authenticate. |
| Token | Short-lived Pine Labs payment token created by the client SDK after a 402 challenge. |
| Credential | Client retry header: P3P-Credential: Payment <payload>. |
| Capture | Server-side debit request to Pine Labs P3P. |
| Mandate Balance | ReservePay authorization balance returned by GET /mpp/v1/balance. |
| Receipt | Server response header: Payment-Receipt: Payment <payload>. |
| Grant | Grantex JWT issued after human consent — scopes and budget that gate an agent's ability to initiate payments. |
Common Issues And Fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Grantex callback cannot find pending checkout | Callback omitted authRequestId | Add merchant state to redirect URI and store pending grant by state. |
| Grant verification fails with agent mismatch | Raw ag_... used for verification | Use DID-form did:grantex:ag_... in server Grantex verifier config. |
| Grant Budget Exceeded | Budget allocated in rupees but SDK debits paise | Allocate initialBudget in paise. Do not divide by 100. |
| Transaction limit exceeded | Cart amount exceeds concrete consent scope | Request mpp:payment:max_txn_paise:<amountPaise> for the cart amount or a higher cap. |
| Mandate balance lookup rejects request | Phone number missing or not 10 digits | Send both authorizationId and 10-digit phoneNumber when available. |
Debit returns FAILED with generic reason | Pine Labs terminal provider decline or sandbox test-data issue | Inspect debit status, mandate balance, active mandate id, and share IDs with Pine Labs support. |
| Browser exposes secrets | p3p-client-sdk instantiated in frontend with clientSecret | Move P3P client SDK usage to the backend. |
