Authentication

Every Plugipay API request is signed with HMAC-SHA256 over a canonical string built from the method, path, body, and timestamp. The SDK does this for you on every call — you only need to hand it your keyId and secret. This page covers how to give them to the client, where to store them, and how platform partners scope calls to a specific merchant.

If you'd like to understand the underlying signing recipe (you don't need to in order to use the SDK), see API authentication.

Constructing the client

PlugipayClient takes a small options bag:

import { PlugipayClient } from '@forjio/plugipay-node';

const plugipay = new PlugipayClient({
  keyId: process.env.PLUGIPAY_KEY_ID!,
  secret: process.env.PLUGIPAY_SECRET!,
  baseUrl: process.env.PLUGIPAY_BASE_URL,     // optional
  onBehalfOf: undefined,                       // optional, platform-admin only
  timeoutMs: 30_000,                           // optional, default 30s
});

The full options shape:

Option Type Required Default Notes
keyId string yes Access key ID, e.g. pk_test_AKIAxxx... or pk_live_AKIAxxx...
secret string yes The HMAC secret, e.g. sk_test_xxx.... Treat like a password.
baseUrl string no https://plugipay.com Useful for staging or a self-hosted test instance.
onBehalfOf string no none An acc_... to scope all calls against. Platform-admin keys only.
timeoutMs number no 30000 Per-request timeout in ms. Applies to each fetch call.

keyId and secret are validated synchronously at construction time. If either is missing, the constructor throws:

Error: PlugipayClient: keyId and secret are required

No network call happens until you call a resource method.

Env-var conventions

The SDK doesn't read env vars on its own. The convention across Plugipay docs, the CLI, and our own example code is:

Variable Purpose
PLUGIPAY_KEY_ID The access key ID.
PLUGIPAY_SECRET The HMAC secret.
PLUGIPAY_BASE_URL Override the base URL. Useful for staging.

A typical bootstrap:

const plugipay = new PlugipayClient({
  keyId: process.env.PLUGIPAY_KEY_ID!,
  secret: process.env.PLUGIPAY_SECRET!,
  baseUrl: process.env.PLUGIPAY_BASE_URL,  // undefined → defaults to https://plugipay.com
});

Never commit the secret to source control. The secret is shown once when you mint the key and there's no recovery flow. Stash it in .env (gitignored), in your platform's secret manager (AWS Secrets Manager, GCP Secret Manager, Vercel envs, etc.), or in a vault.

If you need to confirm the env vars are populated before constructing the client, fail loudly:

function required(name: string): string {
  const v = process.env[name];
  if (!v) throw new Error(`Missing required env var: ${name}`);
  return v;
}

const plugipay = new PlugipayClient({
  keyId: required('PLUGIPAY_KEY_ID'),
  secret: required('PLUGIPAY_SECRET'),
});

Test mode vs live mode

The _test_ and _live_ prefix on the key encodes the environment:

  • pk_test_* + sk_test_* — sandbox. No real money moves.
  • pk_live_* + sk_live_* — production. Real charges.

Both modes share the same base URL (https://plugipay.com). The key alone determines which mode you're in. Test and live data are fully isolated — you can't list test customers with a live key, or vice versa.

A common pattern is to switch keys by env:

const isProd = process.env.NODE_ENV === 'production';

const plugipay = new PlugipayClient({
  keyId: isProd ? process.env.PLUGIPAY_LIVE_KEY_ID! : process.env.PLUGIPAY_TEST_KEY_ID!,
  secret: isProd ? process.env.PLUGIPAY_LIVE_SECRET! : process.env.PLUGIPAY_TEST_SECRET!,
});

How signing works (in brief)

When you call e.g. plugipay.customers.create({ ... }), the SDK:

  1. Serializes the body to JSON (canonical compact form — the same bytes go on the wire).
  2. Generates a fresh idempotency key for mutating calls.
  3. Computes bodyHash = sha256(body).
  4. Builds the string-to-sign: METHOD\npath\ntimestamp\nbodyHash[\nidempotencyKey].
  5. Signs it: signature = hex(HMAC-SHA256(secret, stringToSign)).
  6. Sends the request with:
Authorization: Plugipay-HMAC-SHA256 keyId=<id>, scope=*, signature=<hex>
X-Plugipay-Timestamp: <epoch seconds>
Idempotency-Key: idem_<uuid>     (mutating calls only)
Content-Type: application/json   (when a body is present)

You don't need to think about any of this. The API authentication page covers the recipe in full if you want to dig into it.

Per-call onBehalfOf (platform partners)

If your key has the plugipay:platform:admin scope (issued to Storlaunch, Fulkruma, Ripllo, etc.), you can act on behalf of any merchant workspace by passing their accountId.

There are two ways to scope:

Option A: Default for the client

If a single client only ever serves one merchant, set onBehalfOf at construction:

const plugipay = new PlugipayClient({
  keyId: process.env.PLATFORM_KEY_ID!,
  secret: process.env.PLATFORM_SECRET!,
  onBehalfOf: 'acc_01HXxxxxxxxxxxxxxxxxxxxxxx',
});

await plugipay.checkoutSessions.create({ /* ... */ });
// → header: X-Plugipay-On-Behalf-Of: acc_01HX...

Option B: .forMerchant(accountId)

For platform-admin code that handles many merchants in the same process, mint a clone with a different onBehalfOf:

const platform = new PlugipayClient({
  keyId: process.env.PLATFORM_KEY_ID!,
  secret: process.env.PLATFORM_SECRET!,
});

// One client per merchant — same keys, scoped to a different acc_xxx
const forAlice = platform.forMerchant('acc_01HAlice...');
const forBob = platform.forMerchant('acc_01HBob...');

await forAlice.checkoutSessions.create({ /* ... */ });
await forBob.checkoutSessions.create({ /* ... */ });

forMerchant is cheap — it just copies the options bag with a new onBehalfOf. Don't memoize aggressively; one per request is fine.

Option C: Override per-call

For one-off cross-merchant calls, the low-level request method accepts an onBehalfOf override (you generally won't reach for this; the high-level resource methods don't expose it). See the source for FetchArgs.onBehalfOf if you really need it.

Custom base URL

For staging, a self-hosted Plugipay instance, or local dev against pnpm dev:

const plugipay = new PlugipayClient({
  keyId: process.env.PLUGIPAY_KEY_ID!,
  secret: process.env.PLUGIPAY_SECRET!,
  baseUrl: 'https://staging.plugipay.com',
});

Trailing slashes are normalized — https://plugipay.com/ and https://plugipay.com work identically.

Timeouts

Every fetch call gets an AbortController armed with timeoutMs. If the server doesn't respond in time, the SDK aborts and throws:

throw new PlugipayError(0, 'timeout', `Plugipay request timed out after 30000ms`);

For long-running operations (large list iterations, big report queries), bump it:

const plugipay = new PlugipayClient({
  keyId: '...',
  secret: '...',
  timeoutMs: 120_000, // 2 minutes
});

For short, latency-sensitive paths (an API endpoint where you'd rather fail fast and let the caller retry), lower it.

Custom HTTP client

The SDK uses the global fetch. We don't expose a hook to swap it for axios, got, or a custom agent. The reasons:

  • Connection pooling: Node 20+'s undici underneath fetch already pools.
  • HMAC signing depends on the exact bytes that go on the wire; swapping the transport breaks signing if the new one transforms the body.
  • Retries: handle these at the call site, not in the transport. See Errors for retry patterns.

If you need a corporate proxy, set HTTPS_PROXY and HTTP_PROXY env vars — undici reads them automatically.

Rotating keys

Mint a new key in the dashboard, deploy it to your config, then revoke the old one. Both work concurrently — there's no atomic swap requirement.

For rotation procedures (CI/CD-driven, multi-env), see Portal → API keys → Rotation.

Common errors

401 invalid_signature

The signature didn't match. Almost always one of:

  • Wrong secret (typo, partial paste, or you pasted the keyId as the secret).
  • Mixing test and live keys (the keyId is pk_test_* but the secret is sk_live_*).

401 invalid_timestamp

Your system clock is more than 5 minutes off server time. Sync with NTP.

403 insufficient_scope

The key authenticates, but doesn't have permission for this operation. Check the key's role in the dashboard.

403 workspace_not_authorized

The key belongs to a different workspace than the resource you're operating on. If you're a platform partner, pass onBehalfOf or use forMerchant().

The full catalog is in API errors.

Next

  • Errors — the PlugipayError shape and retry patterns.
  • Webhooks — verifying inbound events with a different secret.
  • Reference — every method on PlugipayClient.
Plugipay — Payments that don't tax your success