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...). UseMost teams use...or just show the code. No motivational framing. Nojourney,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
CheckoutSessionover the API, and verify a signed webhook on a local endpoint — with workingcurland 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 -vto 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-cliglobally 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 --helpand see the command list. If the binary isn't found, check that your npm globalbinis on$PATH:npm config get prefix # should contain /bin/plugipay
Common pitfall — Node version too old
The CLI requires Node 20+. If
plugipay --versionexits withSyntaxError: Unexpected token, your Node is older. Usenvm install 20 && nvm use 20or upgrade via your package manager.
Pitfall — permissions on global install
EACCESon install means your npm global prefix is system-owned. Either reconfigure npm to use a user directory (npm config set prefix ~/.local) or install viasudoonce. Do notsudoevery 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 whoamireturns your account ID. Credentials are at~/.plugipay/credentials.jsonwithchmod 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):
--api-key AKIA_ID:SECRETinline flag (CI only)PLUGIPAY_ACCESS_KEY_ID+PLUGIPAY_SECRET_ACCESS_KEYenv vars- Active profile in
~/.plugipay/credentials.json ~/.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 loginprintsError: 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 tohuudis.com. ExportHTTPS_PROXY=http://your-proxy:portbefore 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
Click Create access key. Name it something specific:
plugipay-backend-prod,plugipay-ci, etc.Select service scope: Plugipay (required). Optionally restrict by mode (
testonly is safer for local dev).Click Create. The secret is shown once. Copy it immediately.
Save the pair to your project's
.envfile:# .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=testAdd
.envto.gitignoreif 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 .envor usedotenv) 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
testonly cannot create live-mode charges and returns403 mode_not_allowed. For local dev,testis the safe default; flip to a separatelivekey pair when you're ready to ship.
6. Step 4 — Your first charge
Heading
Step 4 · Create a
CheckoutSessionvia API
Goal
Send a real API request, get back a
hostedUrlatplugipay.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
hostedUrlin 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 firesplugipay.checkout_session.completed.v1on the event stream (step 5).
Pitfall — 401 auth_required
Clock skew > 5 minutes between your machine and the server causes
signature_invalid. Runntpdate -q time.google.comor check your system clock. The SDK uses UTC automatically; rawcurlneedsdate -utimestamps.
Pitfall — 409 idempotency_key_in_use
The same
Idempotency-Keywith a different payload returns409. 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
199000means Rp 199,000 (two implicit decimals for IDR-minor). USD uses cents (1000=$10.00). Passing199.00for IDR silently rejects withvalidation_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:
- Raw body. Signature is computed over the unparsed payload. Middleware that JSON-parses before your handler breaks verification.
- Idempotent handler. Plugipay may redeliver an event up to 3 times (outbox retry). Key your side-effects on
event.idor the aggregate ID. - Respond 2xx fast. Return
200within 10 seconds or the outbox treats delivery as failed and queues a retry.
Success checkpoint
You see
plugipay.checkout_session.completed.v1in both the CLI stream and your receiver logs. The receiver returns200 okand 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
verifyWebhookSignaturefrom 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 return200— 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
PortalSessionand redirect. 15-minute signed URL, self-serve updates. → API: PortalSession
Make every write safely retryable
Send
Idempotency-Keyon 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.
- Status: status.plugipay.com — API uptime, gateway incidents, webhook delivery lag.
- References: API · CLI · SDKs · Changelog
- Stuck? Email support@forjio.com with your
requestId. We read every message.
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 asdocs/index.md)/docs/quickstart— merchant quickstart (shipped asdocs/quickstart.md)/docs/concepts#subscription— concepts page (shipped asdocs/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 IAMhttps://huudis.com/webhooks— external, Huudis webhook confighttps://huudis.com/activate— external, device-flow activationhttps://status.plugipay.com— external, status pagemailto: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
HowToschema — each step is aHowToStep; helps Google show step previews for "plugipay quickstart" searches
End of docs/quickstart-dev.md.