Adapters
An adapter is Plugipay's connection to a downstream payment provider — Xendit, Midtrans, PayPal, or the built-in manual adapter for bank-transfer flows. Configuring an adapter is what activates a checkout method: enable the Xendit adapter and qris / va / ewallet start working on checkout sessions; enable PayPal and the paypal method works. This page covers the plugipay.adapters namespace; for the underlying HTTP surface see API: Adapters, and for end-to-end onboarding flows see Concepts → Adapters.
Namespace
plugipay.adapters — every method on this namespace:
plugipay.adapters.list()
plugipay.adapters.updateXendit(config)
plugipay.adapters.updatePaypal(config)
plugipay.adapters.updateMidtrans(config)
plugipay.adapters.updateManual(config)
plugipay.adapters.managedOnboardingState()
plugipay.adapters.startManagedOnboarding(input?)
plugipay.adapters.simulateManagedOnboarding(input?) // test-mode only
There are two flavors of adapter setup: direct (you bring your own provider credentials and PATCH them in via updateXendit / updatePaypal / etc.) and managed (Plugipay handles onboarding with the provider on your behalf via startManagedOnboarding).
Methods
adapters.list
Signature. plugipay.adapters.list(): Promise<AdapterConfig[]>
Returns every adapter known for this workspace and whether it's configured. Unconfigured adapters appear with configured: false; their publicConfig is empty.
import { PlugipayClient } from '@forjio/plugipay-node';
const plugipay = new PlugipayClient({
keyId: process.env.PLUGIPAY_KEY_ID!,
secret: process.env.PLUGIPAY_SECRET!,
});
const adapters = await plugipay.adapters.list();
for (const a of adapters) {
console.log(`${a.kind}: ${a.configured ? 'configured' : 'not set up'}`);
}
adapters.updateXendit / updatePaypal / updateMidtrans / updateManual
Signature. Each update* method is (config: Record<string, unknown>) => Promise<AdapterConfig>.
PUT semantics — the config you pass replaces the stored config in full. The exact shape of config is provider-specific; pass the credentials Plugipay's onboarding doc tells you to. See API: Adapters for the per-provider config schema.
// Xendit direct configuration:
await plugipay.adapters.updateXendit({
publicKey: 'xnd_public_production_...',
secretKey: 'xnd_secret_production_...',
webhookToken: '...',
});
// PayPal:
await plugipay.adapters.updatePaypal({
clientId: 'AY...',
clientSecret: '...',
mode: 'live',
});
// Midtrans:
await plugipay.adapters.updateMidtrans({
serverKey: 'Mid-server-...',
clientKey: 'Mid-client-...',
mode: 'production',
});
// Manual (bank transfer) — set up displayed bank account details:
await plugipay.adapters.updateManual({
bankName: 'Bank Central Asia',
bankAccountNumber: '1234567890',
bankAccountHolder: 'PT Forjio Teknologi Indonesia',
instructions: 'Please include your order ID in the transfer reference.',
});
Secrets in
configare write-only. When youlist()an adapter, onlypublicConfigis returned — the secret keys you PUT are never reflected back. Treat the PUT call as setting state, not as a read-modify-write.
adapters.managedOnboardingState
Signature. plugipay.adapters.managedOnboardingState(): Promise<ManagedOnboardingState>
For workspaces using Plugipay's managed onboarding (where Plugipay sets up the provider account for you), this reports current progress: 'not_started', 'pending' (KYC / verification in flight), 'verified', or 'failed'.
const state = await plugipay.adapters.managedOnboardingState();
console.log(state.state, state.provider, state.details);
adapters.startManagedOnboarding
Signature. plugipay.adapters.startManagedOnboarding(input?): Promise<ManagedOnboardingState>
Kicks off managed onboarding for a provider. Default kind is 'xendit'. The SDK auto-attaches an Idempotency-Key. Pass details for any provider-specific KYC inputs Plugipay needs.
const state = await plugipay.adapters.startManagedOnboarding({
kind: 'xendit',
details: {
businessName: 'PT Acme Indonesia',
businessType: 'pt',
npwp: '01.234.567.8-999.000',
},
});
if (state.state === 'pending') {
console.log('KYC submitted; awaiting Xendit verification (usually 1-2 business days)');
}
adapters.simulateManagedOnboarding
Signature. plugipay.adapters.simulateManagedOnboarding(input?): Promise<ManagedOnboardingState>
Test-mode only. Forces the managed-onboarding flow to a target state for integration testing.
// In your test setup:
await plugipay.adapters.simulateManagedOnboarding({ result: 'verified' });
// Now subsequent `managedOnboardingState()` calls return state: 'verified'.
Calling this in live mode returns 403 forbidden.
Types
type AdapterKind = 'xendit' | 'paypal' | 'midtrans' | 'manual';
interface AdapterConfig {
kind: AdapterKind;
configured: boolean;
publicConfig?: Record<string, unknown>;
updatedAt?: string;
}
interface ManagedOnboardingState {
state: 'not_started' | 'pending' | 'verified' | 'failed';
provider: string;
details?: Record<string, unknown>;
}
For the per-provider publicConfig schemas (what's safe to read, what fields you write but can't read back), see API: Adapters.
Common patterns
Bootstrap a workspace with Xendit direct
You signed up for Xendit yourself; you have the credentials. Wire them in once:
async function configureXendit(env: 'live' | 'test') {
await plugipay.adapters.updateXendit({
publicKey: process.env[`XENDIT_PUBLIC_${env.toUpperCase()}`]!,
secretKey: process.env[`XENDIT_SECRET_${env.toUpperCase()}`]!,
webhookToken: process.env[`XENDIT_WEBHOOK_TOKEN_${env.toUpperCase()}`]!,
});
console.log(`Xendit ${env} configured.`);
}
Check what methods are available before checkout
Before showing your checkout method picker, list adapters and figure out which methods are usable:
async function availableMethods(): Promise<CheckoutMethod[]> {
const adapters = await plugipay.adapters.list();
const methods: CheckoutMethod[] = [];
for (const a of adapters) {
if (!a.configured) continue;
if (a.kind === 'xendit') methods.push('qris', 'va', 'ewallet', 'card');
if (a.kind === 'midtrans') methods.push('qris', 'va', 'ewallet');
if (a.kind === 'paypal') methods.push('paypal');
if (a.kind === 'manual') methods.push('retail');
}
return [...new Set(methods)];
}
Drive managed onboarding from a UI
For a customer-facing setup wizard:
async function startOnboardingForUser(input: {
businessName: string;
businessType: string;
npwp: string;
}) {
const state = await plugipay.adapters.startManagedOnboarding({
kind: 'xendit',
details: input,
});
return state;
}
async function pollOnboardingStatus() {
return plugipay.adapters.managedOnboardingState();
}
In your UI, poll pollOnboardingStatus every minute or react to the corresponding webhook events.
End-to-end test the verified-state path
For tests that need to exercise the post-onboarding flow:
beforeEach(async () => {
await plugipay.adapters.simulateManagedOnboarding({ result: 'verified' });
});
test('checkout works once onboarding is verified', async () => {
const session = await plugipay.checkoutSessions.create({
amount: 10_000_00,
currency: 'IDR',
methods: ['qris'],
successUrl: 'https://example.com/ok',
cancelUrl: 'https://example.com/cancel',
});
expect(session.status).toBe('open');
});
Reset an adapter
There's no delete; the way to "unconfigure" is to PUT an empty config. The exact accepted-as-empty shape varies by provider, but this is the canonical pattern:
async function disableXendit() {
await plugipay.adapters.updateXendit({});
// After this, adapters.list() reports kind: 'xendit', configured: false.
}
Errors
| Code | Status | Cause |
|---|---|---|
validation_error |
400 | Missing required provider field, malformed credential format. |
provider_credentials_invalid |
401 | Credentials don't pass Plugipay's smoke test against the provider. |
forbidden |
403 | Calling simulateManagedOnboarding in live mode, or key lacks adapter-write scope. |
state_conflict |
409 | Starting managed onboarding when already 'verified'. |
Next
- Checkout sessions — the consumer of adapter capabilities.
- Payouts — some adapters (Xendit) also handle disbursements.
- API: Adapters — HTTP reference and per-provider config schemas.