Skip to main content

In-Session Purchases

When a user has an active session in your application, you can offer in-session purchases such as additional reports, data exports, premium features, or session extensions. These are billed against the user's SMART token balance.

Purchase Flow Overview

sequenceDiagram
participant User
participant App as Your Application
participant SDK as Provider SDK
participant API as GW Platform API

User->>App: Clicks "Buy Extra Report"
App->>API: POST /purchases/validate (pre-flight)
API-->>App: { valid: true, tokenCost: 5 }
App->>User: Confirm purchase dialog
User->>App: Confirms purchase
App->>API: POST /purchases { itemId, idempotencyKey }
API->>API: Deduct tokens from balance
API-->>App: { purchaseId, newBalance }
App->>User: Show success, deliver item
SDK->>SDK: Emit purchase.completed event

Available Endpoints

All purchase endpoints require the session JWT as a Bearer token:

Authorization: Bearer <gwSession JWT>
MethodPathDescription
GET/api/sessions/:sessionId/available-itemsList purchasable items and their token costs
GET/api/sessions/:sessionId/balanceCurrent SMART token balance for the session
POST/api/sessions/:sessionId/purchases/validatePre-flight check (balance + item availability)
POST/api/sessions/:sessionId/purchasesExecute a purchase
GET/api/sessions/:sessionId/purchasesPurchase history for the session

Listing Available Items

Fetch the items your application has registered for in-session purchase:

const response = await fetch(
`${apiBase}/api/sessions/${sessionId}/available-items`,
{ headers: { Authorization: `Bearer ${token}` } }
);

const { items } = await response.json();

Response:

{
"items": [
{
"itemId": "item-001",
"name": "Extra Report",
"description": "Generate one additional report",
"tokenCost": 5,
"itemType": "add-on",
"metadata": {}
},
{
"itemId": "item-002",
"name": "Data Export",
"description": "Export results as CSV",
"tokenCost": 10,
"itemType": "add-on",
"metadata": {}
},
{
"itemId": "item-003",
"name": "Deep Analysis",
"description": "Run deep entity analysis",
"tokenCost": 25,
"itemType": "data-pack",
"metadata": {}
}
]
}

Item Types

TypeDescription
add-onSingle-use features (extra report, export, premium search)
data-packBundles of data or analysis capacity
extensionSession time extensions (see Extension)

Checking Balance

Before showing purchase options, check the user's available balance:

const response = await fetch(
`${apiBase}/api/sessions/${sessionId}/balance`,
{ headers: { Authorization: `Bearer ${token}` } }
);

const { balance, currency, orgId } = await response.json();
// { balance: 100, currency: "SMART", orgId: "org-789" }

Pre-Flight Validation

Always validate before purchasing to provide a smooth user experience and avoid errors during the actual transaction:

const response = await fetch(
`${apiBase}/api/sessions/${sessionId}/purchases/validate`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ itemId: 'item-001' }),
}
);

const result = await response.json();
// { valid: true, tokenCost: 5, currentBalance: 100, hasSufficientBalance: true }

If the item is not found, the endpoint returns 404:

{ "error": "ITEM_NOT_FOUND", "message": "Item item-999 not found" }

Executing a Purchase

Once validated, submit the purchase with an idempotency key to prevent duplicate charges:

const response = await fetch(
`${apiBase}/api/sessions/${sessionId}/purchases`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
itemId: 'item-001',
idempotencyKey: `${sessionId}-item-001-${Date.now()}`,
}),
}
);

const result = await response.json();
// { purchaseId: "purchase-1715000000-a1b2c3", itemId: "item-001",
// tokenCost: 5, newBalance: 95, purchasedAt: "2026-05-07T12:00:00.000Z" }

Idempotency

The idempotencyKey prevents double-charging if the request is retried (e.g., due to network issues):

  • Same key, same parameters - Returns the cached result without deducting again
  • Same key, different parameters - Returns 409 IDEMPOTENCY_CONFLICT
{
"error": "IDEMPOTENCY_CONFLICT",
"message": "Same key used with different parameters",
"originalRequest": { "itemId": "item-001" }
}

Purchase History

Retrieve all purchases made during the current session:

const response = await fetch(
`${apiBase}/api/sessions/${sessionId}/purchases`,
{ headers: { Authorization: `Bearer ${token}` } }
);

const { purchases } = await response.json();

Response:

{
"purchases": [
{
"purchaseId": "purchase-1715000000-a1b2c3",
"sessionId": "35iiYDoY1fSwSpYX22H8GP7x61o",
"itemId": "item-001",
"itemName": "Extra Report",
"tokenCost": 5,
"purchasedAt": "2026-05-07T12:00:00.000Z",
"idempotencyKey": "sess-123-item-001-1715000000"
}
]
}

Complete Purchase Flow Example

Here is a complete implementation showing the recommended validate-then-purchase pattern:

async function safePurchase(
sessionId: string,
itemId: string,
bearerToken: string
): Promise<PurchaseResult> {
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${bearerToken}`,
};

// 1. Pre-flight validation
const checkResponse = await fetch(
`/api/sessions/${sessionId}/purchases/validate`,
{ method: 'POST', headers, body: JSON.stringify({ itemId }) }
);

if (!checkResponse.ok) {
const { error, message } = await checkResponse.json();
throw new Error(message || error);
}

const { hasSufficientBalance, tokenCost } = await checkResponse.json();
if (!hasSufficientBalance) {
throw new Error(`Insufficient balance. Need ${tokenCost} SMART tokens.`);
}

// 2. Execute purchase with idempotency key
const idempotencyKey = `${sessionId}-${itemId}-${Date.now()}`;
const purchaseResponse = await fetch(
`/api/sessions/${sessionId}/purchases`,
{
method: 'POST',
headers,
body: JSON.stringify({ itemId, idempotencyKey }),
}
);

if (!purchaseResponse.ok) {
const { error, message } = await purchaseResponse.json();
throw new Error(message || error);
}

return purchaseResponse.json();
}

Error Codes

HTTPError CodeMeaningSuggested Handling
402INSUFFICIENT_BALANCENot enough SMART tokensShow balance, offer top-up link to marketplace
403ORGANIZATION_SUSPENDEDOrganization is suspendedShow suspension notice, contact support
404SESSION_NOT_FOUNDSession ID not recognizedRe-validate JWT, redirect to marketplace
404ITEM_NOT_FOUNDItem ID does not existRefresh available items list
409DUPLICATE_PURCHASEIdempotency key already usedTreat as success, return cached result
409PRICE_CHANGEDItem price changed since checkRefresh price, show new cost to user
409CONCURRENT_PURCHASE_FAILEDAnother purchase in progressRetry after short delay
409IDEMPOTENCY_CONFLICTSame key, different paramsGenerate new idempotency key and retry
410SESSION_EXPIREDSession has endedRedirect to marketplace
422PURCHASE_WINDOW_CLOSEDToo close to session endInform user, offer extension first
429RATE_LIMITEDToo many requestsRetry with exponential back-off
500PURCHASE_FAILEDGeneral server errorLog and surface a user-friendly message
503MARKETPLACE_UNAVAILABLEPlatform temporarily downRetry with exponential back-off

Error Handling Pattern

async function handlePurchaseError(error: PurchaseError) {
switch (error.code) {
case 'INSUFFICIENT_BALANCE':
showModal({
title: 'Insufficient Balance',
message: `You need ${error.required} tokens but only have ${error.currentBalance}.`,
action: { label: 'Add Tokens', href: 'https://platform.generalwisdom.com/tokens' },
});
break;

case 'SESSION_EXPIRED':
showNotification('Your session has expired.');
window.location.href = 'https://platform.generalwisdom.com';
break;

case 'RATE_LIMITED':
await delay(error.retryAfter * 1000);
// Retry the purchase
break;

case 'PURCHASE_WINDOW_CLOSED':
showModal({
title: 'Session Ending Soon',
message: 'Extend your session before making purchases.',
action: { label: 'Extend Session', onClick: () => extendSession() },
});
break;

default:
showNotification(`Purchase failed: ${error.message}`);
}
}

SDK Integration

When using the Provider SDK, purchase events are emitted automatically:

const sdk = new MarketplaceSDK({
applicationId: 'your-app-id',
apiKey: 'gwsk_...',
environment: 'production',
hooks: {
onSessionStart: async (context) => {
// Fetch and display available items
const items = await fetchAvailableItems(context.sessionId);
renderPurchasePanel(items);
},
},
});

Next Steps