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:
- Per-call
context.Context— tightest. Passcontext.WithTimeout(ctx, 5*time.Second)to bound one call. ClientOptions.Timeout— the*http.Clienttimeout. 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.DefaultClientby default. It constructs a fresh*http.ClientwithTimeoutset. This avoids surprises if you've modifiedDefaultClientelsewhere. - If you set
ClientOptions.HTTP,ClientOptions.Timeoutis ignored. Set the timeout on your own*http.Clientdirectly. This matters — if you pass a client without aTimeout, calls can hang indefinitely on a dead connection. http.Transportis 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
- Errors — how every failure mode surfaces.
- Webhooks — verifying inbound deliveries.
- API authentication — the underlying HMAC recipe.