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 — the PUT body becomes the new full configuration. If you only have the public key in your config repo, fetch the existing config from the dashboard before calling Update*.

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

Plugipay — Payments that don't tax your success