Plugipay — Developer Quickstart (/docs/quickstart-dev) Copy

  • Project: plugipay (M4, Forjio split)
  • Source brief: spec/prd.md §4 (F1–F5), §6 (Brand Brief) · spec/api-spec.md §3 (auth, errors, pagination), §4.1 (CheckoutSession), §5.1, §6 (events, webhook envelope) · spec/cli-spec.md §2 (auth), §3.1 (checkout), §3.9 (events) · copy/landing-copy.md (voice anchor, shipped) · copy/docs/quickstart.md (merchant parallel flow)
  • Audience: Developers integrating Plugipay API/CLI. Most read docs before signing up. Backend or full-stack engineers — comfortable with curl, environment variables, and webhook signature verification. Ecosystem norm: English.
  • Voice anchor: copy/landing-copy.md — "Stripe circa 2014, before corporate polish." Three voice rules: (1) admit trade-offs, (2) show with a runnable command, (3) no success theatre.
  • Register: Imperative mood. Terse. Code before prose when the command expresses it faster. No second-person marketing (You'll love..., Your team deserves...). Use Most teams use... or just show the code. No motivational framing. No journey, seamlessly, empowering, unlock, best-in-class, revolutionary, next-generation, ecosystem solution.
  • Technical terms: full English ecosystem norm. No translation layer.

Information architecture

# Block Goal Component
1 Hero Name the surface, set the 10-minute promise, link to merchant track marketing/docs/quickstart-dev/hero.tsx
2 Prerequisites Node 20+, Plugipay account, shell — no pretense marketing/docs/quickstart-dev/prereqs.tsx
3 Step 1–5 Five sections: install → auth → keys → first charge → webhook marketing/docs/quickstart-dev/step-[1..5].tsx
4 Error reference Abbreviated error-code table pulled from api-spec §3.2 marketing/docs/quickstart-dev/errors.tsx
5 Next steps Four onward links: Subscriptions, PortalSession, idempotency, live mode marketing/docs/quickstart-dev/next-steps.tsx
6 Support footer Status page, API reference, CLI reference, contact marketing/docs/quickstart-dev/support-footer.tsx

Six blocks. One page. No progress tracker, no "what you'll learn" motivational card — the table of steps at the top is the TOC.


1. Hero

Left column: eyebrow, headline, subhead, two inline CTAs. Right column on desktop: a <pre> block with the three commands we'll run.

Eyebrow

Getting Started · Developer · 10-minute quickstart

Headline (5 words)

From zero to webhook, in five steps.

Voice note: mirrors the merchant quickstart's 10-minute framing but repositions around the terminal artifact. Dev audience judges promises by commands, not minutes.

Subheadline (one sentence, 27 words)

Install the CLI, authenticate, create your first CheckoutSession over the API, and verify a signed webhook on a local endpoint — with working curl and TypeScript snippets.

Trade-off line (voice rule #1, under subhead)

Plugipay is not a gateway. You still need a Xendit or PayPal account to move real money. We handle everything above the gateway: sessions, subscriptions, dunning, ledger, events, idempotency.

What we'll run (right column, pinned on desktop)

npm install -g @forjio/plugipay-cli
plugipay auth login
plugipay checkout create --amount 199000 --currency IDR --success-url ... --cancel-url ...

Primary CTAs (inline under subhead)

  • Start with step 1#step-1 (brand-700 button — anchors)
  • Not a developer? Read the merchant quickstart/docs/quickstart (text link, secondary)

Rationale: scrolling docs, one tab. Anchor to the first step. Secondary CTA hands off cleanly to the merchant track.


2. Prerequisites

Before #step-1. One block, no sub-headings. Three bullets, no fluff.

Block copy

Before you start.

  • Node.js 20 LTS or newer (node -v to confirm). The CLI ships as an ESM Node package.
  • A Plugipay account. Sign up at plugipay.com — OIDC via Huudis, no credit card, self-serve.
  • A shell. bash, zsh, fish, PowerShell — they all work; examples below use POSIX syntax.

You do not need a Xendit or PayPal account to finish this quickstart. We run everything in test mode. The checkout link you generate in step 4 is real, but the sandbox gateway never charges.

Voice note: the last paragraph is voice rule #1 in miniature — admits the sandbox constraint (not real money) to defend the real benefit (zero setup friction). Matches landing-copy §5 ("Sandbox doesn't charge real money") tone.


3. Step 1 — Install the CLI

Heading

Step 1 · Install the CLI

Goal (one line, italic)

Install @forjio/plugipay-cli globally and confirm the binary is on $PATH.

Actions

npm install -g @forjio/plugipay-cli
plugipay --version

Expected output:

plugipay/1.0.0 (darwin-arm64) node-v20.11.0

Success checkpoint

You can run plugipay --help and see the command list. If the binary isn't found, check that your npm global bin is on $PATH:

npm config get prefix  # should contain /bin/plugipay

Common pitfall — Node version too old

The CLI requires Node 20+. If plugipay --version exits with SyntaxError: Unexpected token, your Node is older. Use nvm install 20 && nvm use 20 or upgrade via your package manager.

Pitfall — permissions on global install

EACCES on install means your npm global prefix is system-owned. Either reconfigure npm to use a user directory (npm config set prefix ~/.local) or install via sudo once. Do not sudo every command — that's a supply-chain risk.


4. Step 2 — Authenticate

Heading

Step 2 · Authenticate via device flow

Goal

Log in with Huudis, store encrypted credentials at ~/.plugipay/credentials.json.

Actions

plugipay auth login

The CLI prints a one-time URL and device code:

Open https://huudis.com/activate and enter code: ABCD-1234
Waiting for authorization...

Open the URL in a browser, paste the code, approve. The CLI prints:

Logged in as you@example.com (acc_01HXZ...)

Confirm identity:

plugipay auth whoami
Account:  acc_01HXZ...
Email:    you@example.com
Region:   ap-southeast-1
Mode:     live
Profile:  default

Success checkpoint

plugipay auth whoami returns your account ID. Credentials are at ~/.plugipay/credentials.json with chmod 600. They never touch shell history or env vars.

Named profiles (for CI, staging, or multiple accounts)

plugipay auth login --profile staging
plugipay --profile staging checkout list

# or via env
export PLUGIPAY_PROFILE=staging

Credential lookup order (api-spec §3.1, cli-spec §2.2):

  1. --api-key AKIA_ID:SECRET inline flag (CI only)
  2. PLUGIPAY_ACCESS_KEY_ID + PLUGIPAY_SECRET_ACCESS_KEY env vars
  3. Active profile in ~/.plugipay/credentials.json
  4. ~/.aws/credentials [plugipay] profile (ops-script fallback)

Pitfall — device code expired

The device code is valid for 10 minutes. If you walk away and the code expires, auth login prints Error: authorization expired. Run 'plugipay auth login' again. and exits with code 2. Just rerun the command.

Pitfall — behind a corporate proxy

If the CLI hangs at Waiting for authorization..., your shell likely has no outbound HTTPS to huudis.com. Export HTTPS_PROXY=http://your-proxy:port before running the command.


5. Step 3 — Get API keys (for SDK, CI, and curl)

Heading

Step 3 · Create access keys for programmatic use

Goal

Generate a long-lived access key pair in Huudis IAM. Use for SDK clients, webhook handlers, and CI.

Why this step (one line)

The CLI device flow in step 2 is for you at a terminal. The SDK, your backend server, and CI pipelines need an access key pair — AKIA... + secret — scoped to the Plugipay service.

Actions

  1. Open huudis.com/iam/access-keys.

  2. Click Create access key. Name it something specific: plugipay-backend-prod, plugipay-ci, etc.

  3. Select service scope: Plugipay (required). Optionally restrict by mode (test only is safer for local dev).

  4. Click Create. The secret is shown once. Copy it immediately.

  5. Save the pair to your project's .env file:

    # .env — do NOT commit
    PLUGIPAY_ACCESS_KEY_ID=AKIA...
    PLUGIPAY_SECRET_ACCESS_KEY=...
    PLUGIPAY_ACCOUNT_ID=acc_01HXZ...
    PLUGIPAY_API_URL=https://plugipay.com/api/v1
    PLUGIPAY_MODE=test
    
  6. Add .env to .gitignore if it isn't already.

Screenshot callout: [Screenshot: Huudis → IAM → Access Keys → Create form]

Success checkpoint

You have AKIA... and its secret saved in .env. Load the env file in your shell (source .env or use dotenv) and confirm both are set:

echo "${PLUGIPAY_ACCESS_KEY_ID:0:4}"  # should print "AKIA"

Pitfall — secret shown once

Huudis does not re-reveal the secret after you close the dialog. If you lose it, revoke the key pair and create a new one. Never share keys over Slack, email, or Git — use a vault (1Password, Bitwarden, aws secretsmanager, vault).

Pitfall — mode mismatch

Access keys are mode-scoped. A key created with test only cannot create live-mode charges and returns 403 mode_not_allowed. For local dev, test is the safe default; flip to a separate live key pair when you're ready to ship.


6. Step 4 — Your first charge

Heading

Step 4 · Create a CheckoutSession via API

Goal

Send a real API request, get back a hostedUrl at plugipay.com/c/cs_..., open it, complete a test payment.

6.1 Option A — TypeScript SDK (preferred)

Install:

npm install @forjio/plugipay-sdk

Create a session:

// scripts/first-charge.ts
import { PlugipayClient } from "@forjio/plugipay-sdk";

const plugipay = new PlugipayClient({
  accessKeyId: process.env.PLUGIPAY_ACCESS_KEY_ID!,
  secretAccessKey: process.env.PLUGIPAY_SECRET_ACCESS_KEY!,
  accountId: process.env.PLUGIPAY_ACCOUNT_ID!,
  mode: "test", // or omit to default to live
});

const session = await plugipay.checkoutSessions.create({
  amount: 199000,            // minor units — Rp 199,000
  currency: "IDR",
  methods: ["qris", "va", "ewallet", "card"],
  successUrl: "https://example.com/thanks?s={CHECKOUT_SESSION_ID}",
  cancelUrl: "https://example.com/cancel",
  metadata: { orderId: "ord_123" },
});

console.log(session.hostedUrl);
// → https://plugipay.com/c/cs_01HXZ...

Run:

npx tsx scripts/first-charge.ts

The SDK handles HMAC SigV4 signing, idempotency-key generation, retries on 503 gateway_unavailable, and typed responses. Read the full client in @forjio/plugipay-sdk.

6.2 Option B — CLI one-liner

If you prefer the terminal:

plugipay checkout create \
  --amount 199000 \
  --currency IDR \
  --success-url https://example.com/thanks \
  --cancel-url https://example.com/cancel \
  --methods qris,va,ewallet,card \
  --mode test \
  --json

Output (truncated):

{
  "id": "cs_01HXZ...",
  "status": "open",
  "amount": 199000,
  "currency": "IDR",
  "hostedUrl": "https://plugipay.com/c/cs_01HXZ...",
  "expiresAt": "2026-04-20T12:34:56.789Z"
}

6.3 Option C — Raw curl (what's on the wire)

Use this to inspect the HTTP contract. Don't hand-sign in production — use the SDK.

curl -X POST https://plugipay.com/api/v1/checkout-sessions \
  -H "Authorization: FORJIO4-HMAC-SHA256 Credential=${PLUGIPAY_ACCESS_KEY_ID}/20260419/ap-southeast-1/plugipay/forjio4_request, SignedHeaders=host;x-forjio-date;x-forjio-account, Signature=<hex>" \
  -H "X-Forjio-Date: 20260419T123456Z" \
  -H "X-Forjio-Account: ${PLUGIPAY_ACCOUNT_ID}" \
  -H "X-Forjio-Mode: test" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 199000,
    "currency": "IDR",
    "methods": ["qris", "va", "ewallet", "card"],
    "successUrl": "https://example.com/thanks",
    "cancelUrl": "https://example.com/cancel"
  }'

The request signature (<hex> above) is HMAC-SHA256 of the canonical request — the SDK handles this for you. Full signing spec: api-spec §3.1.

Success checkpoint

Open hostedUrl in a browser. You see the Plugipay hosted checkout page with QRIS, VA, e-wallet, and card options. In test mode, pick any method and follow the sandbox flow — completion fires plugipay.checkout_session.completed.v1 on the event stream (step 5).

Pitfall — 401 auth_required

Clock skew > 5 minutes between your machine and the server causes signature_invalid. Run ntpdate -q time.google.com or check your system clock. The SDK uses UTC automatically; raw curl needs date -u timestamps.

Pitfall — 409 idempotency_key_in_use

The same Idempotency-Key with a different payload returns 409. Either change the key (new UUID) or send the identical payload to replay safely. The SDK generates fresh UUIDv4 per call.

Pitfall — amount is in minor units

199000 means Rp 199,000 (two implicit decimals for IDR-minor). USD uses cents (1000 = $10.00). Passing 199.00 for IDR silently rejects with validation_error. See api-spec §3.4 for the full currency table.


7. Step 5 — Handle the webhook

Heading

Step 5 · Receive and verify plugipay.checkout_session.completed.v1

Goal

Observe the event live with the CLI, then write a receiver that verifies the signature and processes the payload.

7.1 Option A — CLI streaming (for development)

In a separate terminal:

plugipay events listen \
  --types plugipay.checkout_session.completed.v1 \
  --mode test \
  --json

Complete a payment at the hostedUrl from step 4. Within seconds, the CLI prints the event:

{
  "id": "evt_01HXZ...",
  "type": "plugipay.checkout_session.completed.v1",
  "apiVersion": "2026-04-19",
  "account": "acc_01HXZ...",
  "created": "2026-04-19T12:34:56.789Z",
  "livemode": false,
  "data": {
    "object": {
      "id": "cs_01HXZ...",
      "status": "completed",
      "amount": 199000,
      "currency": "IDR",
      "adapter": "xendit",
      "paymentId": "xnd_...",
      "completedAt": "2026-04-19T12:34:56.789Z",
      "hostedUrl": "https://plugipay.com/c/cs_01HXZ...",
      "successUrl": "https://example.com/thanks",
      "cancelUrl": "https://example.com/cancel"
    }
  },
  "request": { "id": "req_01HXZ...", "idempotencyKey": "..." }
}

Ctrl+C exits cleanly and prints the last event ID for resumption via --since evt_....

7.2 Option B — Real webhook receiver

Create a webhook subscription in the Huudis dashboard (huudis.com/webhooks — Plugipay does not mint its own webhook config, per ADR-0006). Point it at your endpoint, e.g. https://api.yourapp.com/webhooks/plugipay.

Minimal Node.js receiver (Express):

// webhooks/plugipay.ts
import express from "express";
import { verifyWebhookSignature } from "@forjio/huudis-node";

const app = express();

app.post(
  "/webhooks/plugipay",
  express.raw({ type: "application/json" }), // raw body required for signature
  (req, res) => {
    const signature = req.header("x-forjio-signature")!;
    const timestamp = req.header("x-forjio-timestamp")!;

    try {
      verifyWebhookSignature({
        payload: req.body,           // Buffer — do NOT JSON.parse first
        signature,
        timestamp,
        secret: process.env.PLUGIPAY_WEBHOOK_SECRET!,
        toleranceSeconds: 300,
      });
    } catch (err) {
      return res.status(400).send("invalid signature");
    }

    const event = JSON.parse(req.body.toString());

    if (event.type === "plugipay.checkout_session.completed.v1") {
      const session = event.data.object;
      // idempotent handler — key off event.id OR session.id
      await markOrderPaid(session.id, session.amount, session.currency);
    }

    res.status(200).send("ok");
  },
);

Three non-negotiables:

  1. Raw body. Signature is computed over the unparsed payload. Middleware that JSON-parses before your handler breaks verification.
  2. Idempotent handler. Plugipay may redeliver an event up to 3 times (outbox retry). Key your side-effects on event.id or the aggregate ID.
  3. Respond 2xx fast. Return 200 within 10 seconds or the outbox treats delivery as failed and queues a retry.

Success checkpoint

You see plugipay.checkout_session.completed.v1 in both the CLI stream and your receiver logs. The receiver returns 200 ok and the event is marked delivered in the Plugipay dashboard under Events → Deliveries.

Pitfall — signature verification fails silently

Never skip signature verification "just for now." The unprotected endpoint is a replay-attack vector. Use verifyWebhookSignature from day one.

Pitfall — infinite retry loop

If your handler throws or returns 5xx, Plugipay retries with exponential backoff (15s, 1m, 5m). Catch handler errors, log them, and still return 200 — then investigate async. Otherwise a single bug stalls your event pipeline.

Pitfall — clock skew on toleranceSeconds

The default 300s tolerance protects against replay. If your server clock drifts more than 5 minutes, signatures fail. Use NTP (chrony, systemd-timesyncd, or cloud-provider default).


8. Error reference

Abbreviated — full list at api-spec §3.2.

HTTP   error.code                     Meaning                                  Safe to retry?
400    validation_error               Malformed request or missing field       No — fix input
401    auth_required                  Missing or malformed Authorization       No — fix auth
401    signature_invalid              Signature or clock skew failure          No — check clock / secret
403    mode_not_allowed               Key scope doesn't permit this mode       No — use correct key
404    not_found                      Resource doesn't exist in this account   No
409    conflict                       State transition not allowed             No
409    idempotency_key_in_use         Same key, different payload              No — new UUID
422    gateway_error                  Xendit or PayPal rejected                Sometimes — inspect gatewayCode
429    rate_limited                   Over req/min budget                      Yes — respect Retry-After
429    quota_exceeded                 Plan limit hit (e.g. Starter GMV)        No — upgrade plan
503    gateway_unavailable            Circuit-broken; transient                Yes — exponential backoff

Response shape (api-spec §3.2):

{
  "error": {
    "code": "validation_error",
    "message": "currency must be one of: IDR, USD",
    "param": "currency",
    "requestId": "req_01HXZ..."
  }
}

Always log requestId — it's the fastest way to get support to find the exact call.


9. Next steps

Four paths. Each is one sentence + one link.

Build recurring revenue

Subscriptions, invoices, prorations, and dunning — all driven by Plan. → Concepts: Plan → Subscription → Invoice

Let buyers manage their own plan

Create a PortalSession and redirect. 15-minute signed URL, self-serve updates. → API: PortalSession

Make every write safely retryable

Send Idempotency-Key on every mutating request. Replays return the cached response. → API: Idempotency

Ship to production

Flip from test keys to live keys. Webhook subscription + signature verification stay identical. → Guide: Going live


10. Support footer

Three lines, left-aligned, muted text.

Voice note: no "We're here 24/7." No "We love hearing from you." Just the practical truth — include the request ID, we'll find the call. Voice rule #3 (no success theatre) in support framing.


Placement notes

Every copy string maps to one of the future frontend components below. Route groups use Next.js convention: (marketing) is a group, not a URL segment. Path: code/frontend/src/app/(marketing)/docs/quickstart-dev/page.tsx (does not exist yet — Ori to create).

Copy string / section Route Component file
Page shell /docs/quickstart-dev (marketing)/docs/quickstart-dev/page.tsx
Hero (eyebrow, headline, subhead) /docs/quickstart-dev#hero marketing/docs/quickstart-dev/hero.tsx
"What we'll run" code block inline in Hero marketing/docs/quickstart-dev/hero.tsx
Prerequisites block #prerequisites marketing/docs/quickstart-dev/prereqs.tsx
Step 1 — Install the CLI #step-1 marketing/docs/quickstart-dev/step-1-install.tsx
Step 2 — Authenticate #step-2 marketing/docs/quickstart-dev/step-2-auth.tsx
Step 3 — Get API keys #step-3 marketing/docs/quickstart-dev/step-3-keys.tsx
Step 4 — First charge (TS/CLI/curl) #step-4 marketing/docs/quickstart-dev/step-4-charge.tsx
Step 5 — Handle the webhook #step-5 marketing/docs/quickstart-dev/step-5-webhook.tsx
Error reference block #errors marketing/docs/quickstart-dev/errors.tsx
Next steps #next-steps marketing/docs/quickstart-dev/next-steps.tsx
Support footer #support marketing/docs/quickstart-dev/support-footer.tsx

Shared primitives (reused across /docs surface):

  • Code block with copy button → components/docs/code-block.tsx
  • Tabbed code (TS / CLI / curl in step 4) → components/docs/code-tabs.tsx
  • Callout variants (goal, success, pitfall) → components/docs/callout.tsx
  • Step number badge → components/docs/step-badge.tsx

Cross-page links that must resolve (create placeholder pages if routes don't exist yet, so the quickstart itself doesn't 404):

  • /docs — docs index (shipped as docs/index.md)
  • /docs/quickstart — merchant quickstart (shipped as docs/quickstart.md)
  • /docs/concepts#subscription — concepts page (shipped as docs/concepts.md — Fumi, parallel subtask)
  • /docs/api — full API reference (placeholder OK for MVP)
  • /docs/api#auth · /docs/api#errors · /docs/api#idempotency · /docs/api#portal-sessions · /docs/api#adr-0006 · /docs/api#amounts — anchors within API reference
  • /docs/api/sdk-node — SDK reference (placeholder OK for MVP)
  • /docs/cli — CLI reference (placeholder OK for MVP)
  • /docs/sdks — SDK index (placeholder OK for MVP)
  • /docs/guides/going-live — live-mode guide (placeholder OK for MVP)
  • /changelog — product changelog (shipped via landing)
  • https://huudis.com/iam/access-keys — external, Huudis IAM
  • https://huudis.com/webhooks — external, Huudis webhook config
  • https://huudis.com/activate — external, device-flow activation
  • https://status.plugipay.com — external, status page
  • mailto:support@forjio.com — support email

SEO/meta for (marketing)/docs/quickstart-dev/page.tsx:

  • <title> — "Developer quickstart · Plugipay docs"
  • <meta name="description"> — "Install the CLI, authenticate via Huudis, create your first CheckoutSession, verify a signed webhook. Ten-minute developer quickstart for Plugipay."
  • og:image — reuse docs-default OG card (Iro/Hikari to produce if not present; NOT in scope of this pass)
  • JSON-LD HowTo schema — each step is a HowToStep; helps Google show step previews for "plugipay quickstart" searches

End of docs/quickstart-dev.md.

Plugipay — Payments that don't tax your success