Adapters
An adapter is Plugipay's connector to an upstream payment provider — Xendit (Indonesia), Midtrans (Indonesia), PayPal (cross-border), or the built-in manual adapter for offline reconciliation. Configuring an adapter enables the matching Methods on checkout sessions. The Go SDK exposes the adapter methods behind client.Adapters: list, per-provider update, and the managed-onboarding flow that lets your merchants sign up to a provider through Plugipay's hosted OAuth. For wire shapes and per-provider config keys, see API → Adapters.
Field on the Client
client.Adapters — type *plugipay.AdaptersResource. Installed by NewClient; shares the parent *http.Client, base URL, key, and OnBehalfOf default. Safe for concurrent use.
Methods
List
Signature. func (r *AdaptersResource) List(ctx context.Context) ([]AdapterConfig, error)
Returns one AdapterConfig per known adapter kind (Xendit, PayPal, Midtrans, Manual). Configured tells you whether the workspace has credentials saved; PublicConfig exposes non-secret fields (e.g. the configured PayPal email) safe for UI display.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cfgs, err := client.Adapters.List(ctx)
if err != nil {
return err
}
for _, cfg := range cfgs {
log.Printf("%-10s configured=%t %v", cfg.Kind, cfg.Configured, cfg.PublicConfig)
}
UpdateXendit / UpdatePaypal / UpdateMidtrans / UpdateManual
cfg, err := client.Adapters.UpdateXendit(ctx, map[string]any{...}) // (*AdapterConfig, error)
cfg, err := client.Adapters.UpdatePaypal(ctx, map[string]any{...})
cfg, err := client.Adapters.UpdateMidtrans(ctx, map[string]any{...})
cfg, err := client.Adapters.UpdateManual(ctx, map[string]any{...})
Per-provider config — map[string]any because the keys differ. Update* is a PUT: send the full intended state, not a partial. The server validates the secret with a smoke-test call to the provider before saving; bad credentials fail with *plugipay.Error{Status: 400, Code: "validation_error"} and the existing config is preserved.
cfg, err := client.Adapters.UpdateXendit(ctx, map[string]any{
"secretKey": "xnd_live_...",
"publicKey": "xnd_public_...",
"webhookKey": "...",
"callbackToken": "...",
})
if err != nil {
return err
}
log.Printf("xendit configured: %t", cfg.Configured)
The per-provider key catalog (which fields each provider expects): API → Adapters.
Update*overwrites the secret. There is no "set the public key but keep the existing secret" path — thePUTbody becomes the new full configuration. If you only have the public key in your config repo, fetch the existing config from the dashboard before callingUpdate*.
ManagedOnboardingState
Signature. func (r *AdaptersResource) ManagedOnboardingState(ctx context.Context) (*ManagedOnboardingState, error)
Returns the current state of the merchant's managed-onboarding flow. Managed onboarding is the Plugipay-hosted path where you redirect the merchant to a provider OAuth (Xendit) instead of asking them to paste API keys. Returns nil Details and State == "not_started" if the merchant hasn't begun.
mo, err := client.Adapters.ManagedOnboardingState(ctx)
if err != nil {
return err
}
log.Printf("provider=%s state=%s", mo.Provider, mo.State)
StartManagedOnboarding
Signature. func (r *AdaptersResource) StartManagedOnboarding(ctx context.Context, in ManagedOnboardingStartInput) (*ManagedOnboardingState, error)
Begins the hosted flow. Kind defaults to AdapterKindXendit if zero-valued. The returned state contains a details["onboardingUrl"] your UI redirects the merchant to. Auto-keyed for idempotency.
mo, err := client.Adapters.StartManagedOnboarding(ctx, plugipay.ManagedOnboardingStartInput{
Kind: plugipay.AdapterKindXendit,
})
if err != nil {
return err
}
if url, ok := mo.Details["onboardingUrl"].(string); ok {
http.Redirect(w, r, url, http.StatusSeeOther)
}
SimulateManagedOnboarding (test mode only)
Signature. func (r *AdaptersResource) SimulateManagedOnboarding(ctx context.Context, in ManagedOnboardingSimulateInput) (*ManagedOnboardingState, error)
Skips the provider round-trip and advances the state machine in test mode. Result defaults to "verified" if empty; "failed" is also valid. Only usable with test-mode keys — live mode rejects with 403.
mo, err := client.Adapters.SimulateManagedOnboarding(ctx, plugipay.ManagedOnboardingSimulateInput{
Result: "verified",
})
Use this in E2E tests so you don't depend on the upstream provider being reachable.
Types
type AdapterConfig struct {
Kind AdapterKind `json:"kind"` // xendit | paypal | midtrans | manual
Configured bool `json:"configured"`
PublicConfig map[string]any `json:"publicConfig,omitempty"`
UpdatedAt string `json:"updatedAt,omitempty"`
}
type ManagedOnboardingState struct {
State string `json:"state"` // not_started | pending | verified | failed
Provider string `json:"provider"` // "xendit"
Details map[string]any `json:"details,omitempty"`
}
type ManagedOnboardingStartInput struct {
Kind AdapterKind `json:"kind"`
Details map[string]any `json:"details,omitempty"`
}
type ManagedOnboardingSimulateInput struct {
Result string `json:"result"` // "verified" | "failed"
}
Typed AdapterKind constants:
plugipay.AdapterKindXendit
plugipay.AdapterKindPaypal
plugipay.AdapterKindMidtrans
plugipay.AdapterKindManual
Common patterns
Render an "adapter setup" panel
cfgs, err := c.Adapters.List(ctx)
if err != nil { return err }
ready := []string{}
needsSetup := []string{}
for _, cfg := range cfgs {
if cfg.Configured {
ready = append(ready, string(cfg.Kind))
} else {
needsSetup = append(needsSetup, string(cfg.Kind))
}
}
log.Printf("ready: %v needs setup: %v", ready, needsSetup)
Drive a "start onboarding" CTA for each needsSetup row.
Hosted onboarding flow
// Step 1 — "Connect Xendit" button:
func startHandler(c *plugipay.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
mo, err := c.Adapters.StartManagedOnboarding(ctx, plugipay.ManagedOnboardingStartInput{
Kind: plugipay.AdapterKindXendit,
})
if err != nil { http.Error(w, "start failed", 502); return }
url, _ := mo.Details["onboardingUrl"].(string)
http.Redirect(w, r, url, http.StatusSeeOther)
}
}
// Step 2 — provider redirects back; poll state to update UI:
func statusHandler(c *plugipay.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
mo, err := c.Adapters.ManagedOnboardingState(r.Context())
if err != nil { http.Error(w, "state failed", 502); return }
json.NewEncoder(w).Encode(mo)
}
}
In production, subscribe to the adapter.onboarding.verified webhook instead of polling.
Errors with errors.As
cfg, err := c.Adapters.UpdateXendit(ctx, in)
var pe *plugipay.Error
if errors.As(err, &pe) {
switch pe.Code {
case "validation_error":
// bad keys — provider rejected the smoke test
case "provider_error":
// Xendit was unreachable; retry later
}
}
Context cancellation
ctx, cancel := context.WithTimeout(parentCtx, 8*time.Second)
defer cancel()
mo, err := c.Adapters.StartManagedOnboarding(ctx, in)
Start is auto-keyed for idempotency, so a timeout-then-retry is safe.
Errors
Code |
Status |
Cause |
|---|---|---|
validation_error |
400 | Bad credentials, unknown adapter kind, smoke-test rejected. |
provider_error |
502 | Upstream provider unreachable. |
not_found |
404 | Unknown adapter kind on a per-kind path. |
forbidden |
403 | SimulateManagedOnboarding called in live mode. |
insufficient_scope |
403 | Key lacks plugipay:adapter:write. |
Full mechanics: Errors.
Next
- Checkout sessions —
Methodsmust be supported by a configured adapter. - Workspaces — adapters are per workspace.
- API → Adapters — HTTP-level reference + per-provider key tables.