The sdk.topups resource manages fiat-to-balance deposits when OFFER-HUB is configured with PAYMENT_PROVIDER=airtm. AirTM top-ups allow users to fund their balances using 200+ local payment methods (bank transfers, mobile money, cash points). The top-up flow is asynchronous: the SDK generates a checkout URL, the user confirms payment in AirTM, and the Orchestrator receives an inbound webhook to credit the user's balance.
Controller: apps/api/src/modules/topups/topups.controller.ts
DTOs:
CreateTopupDto (apps/api/src/modules/topups/dto/create-topup.dto.ts)
ListTopupsDto (apps/api/src/modules/topups/dto/list-topups.dto.ts)
Service: apps/api/src/modules/topups/topups.service.ts
Database Model: Topup in packages/db/prisma/schema.prisma
Top-up State Machine
State Description TOPUP_CREATEDTop-up request initiated TOPUP_AWAITING_USER_CONFIRMATIONWaiting for user to complete checkout via confirmation URI TOPUP_PROCESSINGPayment submitted, AirTM network verifying settlement TOPUP_SUCCEEDEDPayment confirmed by AirTM; user balance credited TOPUP_FAILEDPayment rejected or abandoned by user TOPUP_CANCELEDTop-up cancelled by user or platform before payment completion
Resource Overview
Common Types
type TopupStatus =
| 'TOPUP_CREATED'
| 'TOPUP_AWAITING_USER_CONFIRMATION'
| 'TOPUP_PROCESSING'
| 'TOPUP_SUCCEEDED'
| 'TOPUP_FAILED'
| 'TOPUP_CANCELED';
interface Topup {
id: string; // e.g. "top_01HXYZ789"
userId: string; // e.g. "usr_abc123"
amount: string; // e.g. "100.00"
currency: string; // e.g. "USD"
status: TopupStatus;
airtm?: {
confirmationUri?: string; // Redirect URI for buyer checkout
paymentUrl?: string; // Web URL alias
airtmPayinId?: string; // AirTM internal payin ID
expiresAt?: string; // ISO-8601 expiration timestamp
};
createdAt: string; // ISO-8601 string
updatedAt: string; // ISO-8601 string
}
Methods
Initiates an AirTM top-up payin request. Returns the top-up entity along with the AirTM confirmationUri to which the user must be redirected.
Signature
async create(params: CreateTopupParams): Promise<Topup>
Parameters
params: CreateTopupParams
Parameter Type Required Description userIdstringTarget OFFER-HUB user ID (must have a linked AirTM account) amountstringAmount to deposit (minimum $5.00 USD) currencystringCurrency code (e.g. 'USD')
Return Type
Promise<Topup>
Errors
Error Class HTTP Status Code Cause ValidationError400AMOUNT_BELOW_MINIMUMRequested amount is below the AirTM minimum threshold ($5.00) ValidationError422AIRTM_USER_NOT_LINKEDUser has not linked an AirTM account via sdk.users.linkAirtm() NotFoundError404USER_NOT_FOUNDSpecified userId does not exist OfferHubError503AIRTM_UNAVAILABLEAirTM Enterprise API endpoint unreachable or returned an error
Example
import { OfferHubSDK, ValidationError } from '@offerhub/sdk';
const sdk = new OfferHubSDK({
apiUrl: process.env.OFFERHUB_API_URL!,
apiKey: process.env.OFFERHUB_API_KEY!,
});
async function startDeposit(userId: string, amount: string) {
try {
const topup = await sdk.topups.create({
userId,
amount,
currency: 'USD',
});
console.log('Top-up created:', topup.id);
console.log('Status:', topup.status);
// Redirect the user to complete payment on AirTM
if (topup.airtm?.confirmationUri) {
window.location.href = topup.airtm.confirmationUri;
}
return topup;
} catch (error) {
if (error instanceof ValidationError) {
console.error('Validation error:', error.details);
} else {
console.error('Failed to initiate top-up:', error);
}
}
}
Retrieves a paginated list of top-ups, with optional filtering by user ID and status.
Signature
async list(params?: ListTopupsParams): Promise<PaginatedTopups>
Parameters
params?: ListTopupsParams
Parameter Type Required Description userIdstringNo Filter by specific user ID statusTopupStatusNo Filter by top-up lifecycle status limitnumberNo Results per page (default: 20, max: 100) cursorstringNo Pagination cursor
Return Type
interface PaginatedTopups {
items: Topup[];
pagination: {
hasMore: boolean;
nextCursor?: string | null;
total?: number;
};
}
Errors
Error Class HTTP Status Code Cause ValidationError400INVALID_FILTERMalformed pagination parameter or invalid status enum value
Example
const pendingTopups = await sdk.topups.list({
userId: 'usr_abc123',
status: 'TOPUP_AWAITING_USER_CONFIRMATION',
limit: 10,
});
console.log(`Found ${pendingTopups.items.length} pending top-ups.`);
for (const item of pendingTopups.items) {
console.log(`- ${item.id}: $${item.amount} ${item.currency}`);
}
Fetches a single top-up record by its unique identifier.
Signature
async get(id: string): Promise<Topup>
Parameters
Parameter Type Required Description idstringTop-up unique identifier (top_...)
Return Type
Promise<Topup>
Errors
Error Class HTTP Status Code Cause NotFoundError404TOPUP_NOT_FOUNDTop-up ID does not exist
Example
const topup = await sdk.topups.get('top_xyz789');
if (topup.status === 'TOPUP_SUCCEEDED') {
console.log('Top-up has completed successfully.');
} else {
console.log(`Current top-up status: ${topup.status}`);
}
Queries the upstream AirTM API directly to check payin status. If AirTM confirms the payment or reports a failure, the local database state is updated immediately and topup.succeeded or topup.failed events are emitted.
Signature
async refresh(id: string): Promise<Topup>
Parameters
Parameter Type Required Description idstringTop-up identifier
Return Type
Promise<Topup>
Errors
Error Class HTTP Status Code Cause NotFoundError404TOPUP_NOT_FOUNDTop-up ID does not exist ProviderTimeoutError504PROVIDER_TIMEOUTExternal AirTM API request timed out
Example
const refreshed = await sdk.topups.refresh('top_xyz789');
console.log('Synchronized top-up status:', refreshed.status);
Cancels an active top-up that is in TOPUP_CREATED or TOPUP_AWAITING_USER_CONFIRMATION status.
Signature
async cancel(id: string): Promise<Topup>
Parameters
Parameter Type Required Description idstringTop-up identifier to cancel
Return Type
Promise<Topup> (with status: 'TOPUP_CANCELED')
Errors
Error Class HTTP Status Code Cause NotFoundError404TOPUP_NOT_FOUNDTop-up not found InvalidTransitionError409CANNOT_CANCEL_TOPUPTop-up is already in TOPUP_PROCESSING or TOPUP_SUCCEEDED status
Example
import { OfferHubSDK, InvalidTransitionError } from '@offerhub/sdk';
try {
const canceledTopup = await sdk.topups.cancel('top_xyz789');
console.log(`Top-up ${canceledTopup.id} has been canceled.`);
} catch (error) {
if (error instanceof InvalidTransitionError) {
console.error('Cannot cancel: payment is already being processed or completed.');
}
}
Users Reference (sdk.users) — Linking AirTM accounts (linkAirtm)
Balance Reference (sdk.balance) — Checking credited balances
Deposits Guide — High-level deposit flow and webhooks
Real-Time Events & Webhooks — Listening for topup.succeeded events