Authentication

The Plugipay Go SDK signs every request with HMAC-SHA256 using a key ID + secret pair. This page is about how you give those credentials to the SDK and the knobs around scoping, timeouts, and HTTP transport.

If you want the wire-level signing recipe (for proxies or non-Go integrations), see API authentication. The SDK handles all of that for you.

The credentials

Mint a key pair in Settings → API keys — or see Portal → API keys. You get two values:

Field Format Visibility
Access key ID pk_test_xxxxxxxxxxxxxxxx or pk_live_xxxxxxxxxxxxxxxx Public (safe to log)
Secret sk_test_xxxxxxxxxxxxxxxxxxxxxxxx or sk_live_xxxxxxxxxxxxxxxxxxxxxxxx Secret — shown once

The _test_ and _live_ prefixes encode the environment. Test keys can't touch live data and vice versa.

The secret appears only once. When you create a key, Plugipay shows the secret in a dialog. If you close it without copying, you have to mint a new key — there's no recovery flow.

ClientOptions

NewClient takes one ClientOptions struct. Every field has a sensible default; in production you typically rely on env vars and pass a zero value.

type ClientOptions struct {
    KeyID      string         // PLUGIPAY_KEY_ID
    Secret     string         // PLUGIPAY_SECRET
    BaseURL    string         // PLUGIPAY_BASE_URL, default https://plugipay.com
    OnBehalfOf string         // PLUGIPAY_ON_BEHALF_OF, platform-admin keys only
    Timeout    time.Duration  // default 30s
    HTTP       *http.Client   // default fresh client with Timeout
}

The constructor:

c, err := plugipay.NewClient(plugipay.ClientOptions{})
if err != nil {
    log.Fatal(err)
}

If KeyID or Secret can't be resolved from either the options or the env, NewClient returns *plugipay.Error with Code: "missing_key_id" or Code: "missing_secret".

Env vars (recommended)

export PLUGIPAY_KEY_ID=pk_live_xxxxxxxxxxxxxxxx
export PLUGIPAY_SECRET=sk_live_xxxxxxxxxxxxxxxxxxxxxxxx
c, _ := plugipay.NewClient(plugipay.ClientOptions{})

This is the path 12-factor apps want: secrets live outside your binary, your code is environment-agnostic. The SDK reads:

Env var Field When
PLUGIPAY_KEY_ID KeyID always
PLUGIPAY_SECRET Secret always
PLUGIPAY_BASE_URL BaseURL only set this in dev/staging
PLUGIPAY_ON_BEHALF_OF OnBehalfOf platform-admin contexts

If ClientOptions has a value for a field, the env var is ignored for that field — explicit wins.

Explicit credentials

For tests, CLI tools, or any case where reading from the env is wrong:

c, err := plugipay.NewClient(plugipay.ClientOptions{
    KeyID:  "pk_test_xxxxxxxxxxxxxxxx",
    Secret: "sk_test_xxxxxxxxxxxxxxxxxxxxxxxx",
})

Don't hardcode live secrets. A sk_live_* literal in a source file ends up in your git history and your CI logs. Pull from a secret manager (HashiCorp Vault, AWS Secrets Manager, etc.) or env at startup.

BaseURL and dev environments

Default is https://plugipay.com. Override only if you're hitting a staging or self-hosted instance:

c, _ := plugipay.NewClient(plugipay.ClientOptions{
    KeyID:   os.Getenv("PLUGIPAY_KEY_ID"),
    Secret:  os.Getenv("PLUGIPAY_SECRET"),
    BaseURL: "https://staging.plugipay.com",
})

The SDK trims any trailing slash — "https://plugipay.com/" and "https://plugipay.com" both work.

Timeouts

Two layers, in order of priority:

  1. Per-call context.Context — tightest. Pass context.WithTimeout(ctx, 5*time.Second) to bound one call.
  2. ClientOptions.Timeout — the *http.Client timeout. Default 30s.
c, _ := plugipay.NewClient(plugipay.ClientOptions{
    Timeout: 10 * time.Second, // every call has at most 10s
})

A canceled context surfaces as *plugipay.Error{Code: "canceled"}. A deadline exceeded becomes Code: "timeout". See Errors.

OnBehalfOf — platform-admin keys

If you hold a Plugipay platform-admin key (e.g. you're Storlaunch / Fulkruma / Ripllo) and you call the API on behalf of merchant workspaces:

master, _ := plugipay.NewClient(plugipay.ClientOptions{
    KeyID:  os.Getenv("PLUGIPAY_PLATFORM_KEY_ID"),
    Secret: os.Getenv("PLUGIPAY_PLATFORM_SECRET"),
})

for _, accountID := range merchantAccountIDs {
    scoped := master.ForMerchant(accountID)
    invs, err := scoped.Invoices.List(ctx, plugipay.InvoiceListParams{})
    // ...
}

ForMerchant(accountID) returns a shallow clone of the client with X-Plugipay-On-Behalf-Of: <accountID> set on every request. The underlying *http.Client is shared, so connection pooling still works across merchants.

You can also set a default at construction:

c, _ := plugipay.NewClient(plugipay.ClientOptions{
    OnBehalfOf: "acc_01HXX...",
})

OnBehalfOf is silently ignored when used with a regular merchant key — only platform-admin keys can act on behalf of other accounts.

Custom *http.Client

Pass your own *http.Client to wire in tracing, retries, proxies, or a custom transport:

import (
    "net/http"
    "time"

    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

httpClient := &http.Client{
    Timeout:   30 * time.Second,
    Transport: otelhttp.NewTransport(http.DefaultTransport),
}

c, _ := plugipay.NewClient(plugipay.ClientOptions{
    HTTP: httpClient,
})

A few things to know:

  • The SDK does NOT use http.DefaultClient by default. It constructs a fresh *http.Client with Timeout set. This avoids surprises if you've modified DefaultClient elsewhere.
  • If you set ClientOptions.HTTP, ClientOptions.Timeout is ignored. Set the timeout on your own *http.Client directly. This matters — if you pass a client without a Timeout, calls can hang indefinitely on a dead connection.
  • http.Transport is reusable. One transport per process, many clients per transport. The SDK doesn't care.

Proxy

If you're behind a corporate HTTP proxy:

proxyURL, _ := url.Parse("http://proxy.example.com:8080")
httpClient := &http.Client{
    Timeout: 30 * time.Second,
    Transport: &http.Transport{
        Proxy: http.ProxyURL(proxyURL),
    },
}
c, _ := plugipay.NewClient(plugipay.ClientOptions{HTTP: httpClient})

Or rely on the standard HTTPS_PROXY / HTTP_PROXY env vars — http.DefaultTransport reads them out of the box.

Retries (opinion)

The SDK does not retry on its own. We considered it; the right retry policy depends on whether the request is idempotent, what your downstream user is doing, and how long they'll wait. Building it into the SDK forces opinions.

Instead, wrap your *http.Client with a retrying transport — or retry at the call site:

var (
    inv *plugipay.Invoice
    err error
)
for attempt := 0; attempt < 3; attempt++ {
    inv, err = c.Invoices.Get(ctx, id)
    var pe *plugipay.Error
    if errors.As(err, &pe) && (pe.Code == "rate_limited" || pe.Code == "timeout") {
        time.Sleep(time.Duration(1<<attempt) * time.Second)
        continue
    }
    break
}

See Errors for a more complete pattern.

Inspecting the client

For diagnostics:

c.KeyID()       // "pk_live_xxx" — safe to log
c.BaseURL()     // "https://plugipay.com"
c.OnBehalfOf()  // "acc_..." or ""

Don't log c.Secret() — the SDK doesn't expose a getter for it on purpose.

Multi-workspace / multi-key

If your service holds keys for multiple workspaces, construct one *plugipay.Client per workspace at startup and keep them in a map:

type WorkspaceClients struct {
    clients map[string]*plugipay.Client
    mu      sync.RWMutex
}

func (wc *WorkspaceClients) Get(workspaceID string) *plugipay.Client {
    wc.mu.RLock()
    defer wc.mu.RUnlock()
    return wc.clients[workspaceID]
}

Each *plugipay.Client is goroutine-safe; sharing across handlers is the intended pattern.

Low-level signing

If you're writing a proxy in Go that re-signs requests (rare, but supported), drop down to the exported Sign helper:

sig := plugipay.Sign("sk_live_xxx", plugipay.SignInput{
    Method: "POST",
    Path:   "/api/v1/customers",
    Body:   `{"email":"ada@example.com"}`,
})

req.Header.Set("Authorization", plugipay.AuthorizationHeader("pk_live_xxx", sig.Signature))
req.Header.Set("X-Plugipay-Timestamp", sig.Timestamp)

The signature format is documented in detail at API authentication. Stick to the SDK for normal use.

Next

Plugipay — Payments that don't tax your success