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>
| Method | Path | Description |
|---|---|---|
GET | /api/sessions/:sessionId/available-items | List purchasable items and their token costs |
GET | /api/sessions/:sessionId/balance | Current SMART token balance for the session |
POST | /api/sessions/:sessionId/purchases/validate | Pre-flight check (balance + item availability) |
POST | /api/sessions/:sessionId/purchases | Execute a purchase |
GET | /api/sessions/:sessionId/purchases | Purchase 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
| Type | Description |
|---|---|
add-on | Single-use features (extra report, export, premium search) |
data-pack | Bundles of data or analysis capacity |
extension | Session 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
| HTTP | Error Code | Meaning | Suggested Handling |
|---|---|---|---|
| 402 | INSUFFICIENT_BALANCE | Not enough SMART tokens | Show balance, offer top-up link to marketplace |
| 403 | ORGANIZATION_SUSPENDED | Organization is suspended | Show suspension notice, contact support |
| 404 | SESSION_NOT_FOUND | Session ID not recognized | Re-validate JWT, redirect to marketplace |
| 404 | ITEM_NOT_FOUND | Item ID does not exist | Refresh available items list |
| 409 | DUPLICATE_PURCHASE | Idempotency key already used | Treat as success, return cached result |
| 409 | PRICE_CHANGED | Item price changed since check | Refresh price, show new cost to user |
| 409 | CONCURRENT_PURCHASE_FAILED | Another purchase in progress | Retry after short delay |
| 409 | IDEMPOTENCY_CONFLICT | Same key, different params | Generate new idempotency key and retry |
| 410 | SESSION_EXPIRED | Session has ended | Redirect to marketplace |
| 422 | PURCHASE_WINDOW_CLOSED | Too close to session end | Inform user, offer extension first |
| 429 | RATE_LIMITED | Too many requests | Retry with exponential back-off |
| 500 | PURCHASE_FAILED | General server error | Log and surface a user-friendly message |
| 503 | MARKETPLACE_UNAVAILABLE | Platform temporarily down | Retry 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
- Extend a session when users need more time
- Handle session termination and cleanup
- Receive webhook notifications for purchase events