@appmerge/appactor-sdk
v3.0.1
Published
Official Node.js SDK for AppActor - Manage apps and subscribers with ease
Downloads
792
Maintainers
Readme
AppActor Node.js SDK
Official Node.js SDK for AppActor - Manage apps, subscribers, and token-based subscriptions via server-side API.
Installation
npm install @appmerge/appactor-sdkQuick Start
const AppActor = require('@appmerge/appactor-sdk');
const client = new AppActor('sk_your_project_secret_key');
// Get subscriber info
const user = await client.users.get('user-id');
console.log(user);
// Get project token balance
const tokens = await client.users.getTokenBalances('user-id');
console.log(tokens.tokenBalance.total);
// Debit tokens with project secret + user id. No appId needed.
await client.users.debitTokens('user-id', 5, {
idempotencyKey: 'request-123',
metadata: { feature: 'video-gen' },
});
// App-specific operations still use forApp(appId)
const appClient = client.forApp('your_app_id');
const app = await appClient.apps.get();
console.log(app.name, app.platform);Features
- App Info - Retrieve app details for a selected app
- Users/Subscribers - Look up user info, entitlements, subscriptions, and token balances
- Token Operations - Debit/grant tokens with smart renewable-first priority
- Caching - Automatic 1-hour in-memory cache for app data
- Error Handling - Typed error classes for every failure scenario
- TypeScript - Full type definitions included
- Project-scoped auth - Use one project secret key for customer reads; select an app only when needed
Initialization
const AppActor = require('@appmerge/appactor-sdk');
const client = new AppActor('sk_your_project_secret_key', {
appId: 'your_app_id', // optional, app-specific default
baseUrl: 'https://api.appactor.com', // default
timeout: 30000, // default, in ms
});| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| secretKey | string | Yes | Your project-scoped secret API key (sk_xxx) from Project Settings |
| options.appId | string | No | Target AppActor app id for app-specific methods. Sent as X-AppActor-App-Id only when present |
| options.baseUrl | string | No | API base URL |
| options.timeout | number | No | Request timeout in ms (default: 30000) |
AppActor secret keys are project-scoped. Customer reads and token mutations use only
sk + userId. App-specific operations such as app info require an app context. Useclient.forApp(appId)when you need one.
client.forApp(appId)
Returns an app-scoped client with the same secret key, base URL, and timeout.
const appClient = client.forApp('your_app_id');
await appClient.apps.get();Apps Service
client.apps.get(skipCache?)
Returns app information for the configured appId. Cached for 1 hour. Requires an app-scoped client.
const appClient = client.forApp('your_app_id');
const app = await appClient.apps.get();
// {
// id: '1273925e-ba9e-4fe5-bd02-8c50a9c23ef8',
// projectId: '3f7b3e1b-a98b-4d12-9cdf-6349930cda28',
// name: 'Hair AI',
// bundleId: 'net.appmerge.hairstyle',
// packageName: null,
// platform: 'ios'
// }
// Force fresh data (skip cache)
const freshApp = await appClient.apps.get(true);client.apps.clearCache()
Clears cached app data. Returns true if cache was cleared.
client.apps.getCacheStats()
Returns cache hit/miss statistics.
const stats = client.apps.getCacheStats();
// { hits: 5, misses: 1, keys: 1, ksize: 0, vsize: 0 }Subscribers Service
client.users.get(userId) / client.subscribers.getUser(userId)
Get full subscriber info including entitlements and subscriptions. Project-scoped clients return tokenBalance: null; use getTokenBalances for project token totals.
const subscriber = await client.users.get('appactor-anon-xxx');
// {
// user: {
// id: '019d4ef7-...',
// appUserId: 'appactor-anon-xxx',
// aliases: [...],
// entitlements: {},
// subscriptions: {},
// nonSubscriptions: {},
// tokenBalance: null,
// firstSeenAt: '2026-04-02T16:11:59.195Z',
// lastSeenAt: '2026-04-02T16:19:29.099Z',
// trial: {
// isTrial: true,
// isCanceled: true,
// willRenew: false,
// activeTrialCount: 1,
// canceledTrialCount: 1,
// productId: 'weekly_trial',
// expiresAt: '2026-04-09T16:11:59.195Z'
// }
// }
// }Trial cancellation guard
user.trial is computed by the Node SDK from active subscriptions. A user is treated as an active trial when periodType === 'trial' and the subscription is still active. A trial is treated as canceled when AppActor sees autoRenew === false or unsubscribeDetectedAt.
const subscriber = await client.users.get('appactor-anon-xxx');
if (subscriber.user.trial.isTrial && subscriber.user.trial.isCanceled) {
// Block trial-token spending or require an upgrade.
return { allowed: false, reason: 'trial_canceled' };
}client.users.getTokenBalances(userId)
Get the project token wallet for a user.
const balances = await client.users.getTokenBalances('user-id');
// {
// currencyCode: 'tokens',
// tokenBalance: { renewable: 30, nonRenewable: 10, total: 40 }
// }client.subscribers.getTokens(userId)
Get the project token balance. This is a convenience alias that returns only tokenBalance.
const tokens = await client.subscribers.getTokens('user-id');
// { renewable: 30, nonRenewable: 0, total: 30 }client.users.debitTokens(userId, amount, options?) / client.subscribers.debitTokens(...)
Debit tokens from a user's project wallet. By default, AppActor deducts from renewable tokens first, then non-renewable, atomically on the API side.
// User has: renewable=30, nonRenewable=10
// Debit 5 tokens
const balance = await client.users.debitTokens('user-id', 5, {
idempotencyKey: 'request-123',
metadata: { feature: 'video-gen' },
});
// Result: { renewable: 25, nonRenewable: 10, total: 35 }
// Force a specific bucket if needed
await client.users.debitTokens('user-id', 3, { tokenType: 'non_renewable' });client.users.grantTokens(userId, amount, options) / client.subscribers.grantTokens(...)
Grant project tokens. tokenType is required for grants.
await client.users.grantTokens('user-id', 25, {
tokenType: 'renewable',
idempotencyKey: 'grant-request-123',
metadata: { source: 'support-credit' },
});client.subscribers.debitTokensByAppCost(userId, tokenCost?)
Debit tokens based on a cost value. Pass tokenCost to keep this project-scoped. If tokenCost is omitted, the SDK fetches the selected app's tokenCost, so that fallback requires client.forApp(appId).
// Project-scoped, no app id needed
await client.users.debitTokensByAppCost('user-id', 10);
// Optional app-cost lookup when you intentionally want app settings
const appClient = client.forApp('your_app_id');
await appClient.subscribers.debitTokensByAppCost('user-id');Error Handling
Every error extends AppActorError with a code property for programmatic handling.
const AppActor = require('@appmerge/appactor-sdk');
try {
await client.users.debitTokens('user-id', 100);
} catch (error) {
if (error instanceof AppActor.InsufficientTokensError) {
console.log(`Need ${error.required}, have ${error.available}`);
} else if (error instanceof AppActor.SubscriberNotFoundError) {
console.log(`User not found: ${error.subscriberId}`);
} else if (error instanceof AppActor.ApiError) {
console.log(`API error ${error.statusCode}: ${error.message}`);
}
}Error Classes
| Error | Code | Properties | When |
|-------|------|------------|------|
| AppActorError | varies | code | Base class for all errors |
| AppNotFoundError | APP_NOT_FOUND | appId | App not found |
| AppIdRequiredError | APP_ID_REQUIRED | operation | App-specific method called without app context |
| SubscriberNotFoundError | SUBSCRIBER_NOT_FOUND | subscriberId | User not found |
| InsufficientTokensError | INSUFFICIENT_TOKENS | subscriberId, required, available | Not enough tokens |
| InvalidTokenCostError | INVALID_TOKEN_COST | tokenCost | Token cost is 0 or invalid |
| ApiError | API_ERROR | statusCode, body | HTTP error from API |
Complete Example
const AppActor = require('@appmerge/appactor-sdk');
const client = new AppActor(process.env.APPACTOR_SECRET_KEY);
async function processUserAction(userId) {
try {
// Check token balance
const tokens = await client.users.getTokens(userId);
const tokenCost = 1;
if (tokens.total < tokenCost) {
return { success: false, reason: 'insufficient_tokens', balance: tokens };
}
// Debit tokens
const newBalance = await client.users.debitTokensByAppCost(userId, tokenCost, {
idempotencyKey: `consume:${userId}:${Date.now()}`,
metadata: { feature: 'video-gen' },
});
return { success: true, balance: newBalance };
} catch (error) {
if (error instanceof AppActor.SubscriberNotFoundError) {
return { success: false, reason: 'user_not_found' };
}
throw error;
}
}
// Cleanup when shutting down
process.on('SIGTERM', () => client.destroy());Constants
const { TOKEN_TYPES } = require('@appmerge/appactor-sdk');
TOKEN_TYPES.AUTO // 'auto'
TOKEN_TYPES.RENEWABLE // 'renewable'
TOKEN_TYPES.NON_RENEWABLE // 'non_renewable'TypeScript
Full type definitions are included. Works out of the box with TypeScript projects.
import AppActor, {
App,
Subscriber,
TokenBalance,
ProjectTokenBalances,
SubscriberNotFoundError,
InsufficientTokensError,
} from '@appmerge/appactor-sdk';
const client = new AppActor(process.env.APPACTOR_SECRET_KEY!);
const subscriber: Subscriber = await client.users.get('user-id');
if (subscriber.user.trial.isTrial && subscriber.user.trial.isCanceled) {
throw new Error('Trial was canceled before renewal.');
}
const balances: ProjectTokenBalances = await client.users.getTokenBalances('user-id');
const tokens: TokenBalance = await client.users.getTokens('user-id');
const newBalance: TokenBalance = await client.users.debitTokens('user-id', 5);Requirements
- Node.js >= 18.0.0 (uses native
fetch) - An AppActor project-scoped secret API key (
sk_xxx) - The target AppActor app id only for app-specific operations such as
apps.get()
License
ISC
