@alufie/payuni
v0.1.0
Published
Headless, typed PAYUNi payment, refund, query, recurring, logistics, coupon, and webhook framework.
Maintainers
Readme
@alufie/payuni
Headless, typed PAYUNi framework for Node.js server applications. It provides encryption, request and response validation, payments, queries, refunds, recurring billing, logistics, coupons, settlement queries, verified webhooks, UI-neutral settings metadata, and structured log hooks. It has no runtime dependencies and no UI or Svelte dependency.
The package follows the live PAYUNi documentation as reviewed on 2026-07-24.
The old raw-card /api/credit flow was removed by PAYUNi in July 2026; this
package exposes /api/credit only as the current CreditHash token charge.
Install
pnpm add @alufie/payuniNode.js 22 or newer is required. Keep every client, secret provider, recurring direct-card call, and webhook verifier in server-only code.
Five-minute integration
import {
EnvSecretProvider,
PayuniClient,
resolveEnvironment
} from '@alufie/payuni';
import { payments } from '@alufie/payuni/payments';
const client = new PayuniClient({
environment: resolveEnvironment(process.env.PAYUNI_ENVIRONMENT),
secretProvider: new EnvSecretProvider(process.env)
});
const pay = payments(client);
const checkout = await pay.hosted({
MerTradeNo: 'ORDER-20260724-1',
TradeAmt: 1200,
Timestamp: Math.floor(Date.now() / 1000),
ProdDesc: 'Airway product',
Credit: 1,
ATM: 1,
ReturnURL: 'https://example.com/payment/return',
NotifyURL: 'https://example.com/api/payuni/notify'
});checkout is UI-neutral:
{
action: 'https://sandbox-api.payuni.com.tw/api/upp',
method: 'POST',
enctype: 'application/x-www-form-urlencoded',
fields: {
MerID: '...',
Version: '2.0',
EncryptInfo: '...',
HashInfo: '...'
}
}Render those four fields as hidden inputs in your own form and submit it to
action. The package never emits HTML.
Secrets from a database
The client resolves a provider for each request, so a site can rotate credentials without restarting:
import { PayuniClient, createSecretProvider } from '@alufie/payuni';
const client = new PayuniClient({
environment: 'production',
secretProvider: createSecretProvider(async () => {
const row = await database.getPrivatePaymentSettings();
return {
merchantId: row.payuniMerchantId,
hashKey: row.payuniHashKey,
hashIv: row.payuniHashIv
};
})
});cacheSecretProvider(provider, { ttlMs }) is available when a deliberate,
short-lived credential cache is appropriate. Caching is off by default.
Trusted npm publishing
The repository includes .github/workflows/publish.yml for npm trusted
publishing. It runs the full package checks and publishes on a v* tag or a
manual workflow dispatch using GitHub Actions OIDC; no long-lived npm publish
token is stored in GitHub. Configure npm package settings → Trusted Publisher
with GitHub Actions, owner jmyt8, repository alufie-payuni, workflow
filename publish.yml, and allow npm publish. Because this repository is
private, npm will not attach a provenance attestation; trusted OIDC
authentication still applies.
For direct API calls, result.envelope.Status and
result.envelope.Message contain PAYUNi's outer response metadata, while
result.data contains the hash-verified, decrypted transaction fields. The
client throws PayuniProviderError when PAYUNi returns an error envelope
without encrypted result data.
Feature imports
Every feature is an explicit subpath, so sites include only what they use:
import { payments } from '@alufie/payuni/payments';
import { asynchronous } from '@alufie/payuni/async';
import { queries } from '@alufie/payuni/queries';
import { refunds } from '@alufie/payuni/refunds';
import { recurring } from '@alufie/payuni/recurring';
import { logistics } from '@alufie/payuni/logistics';
import { coupons } from '@alufie/payuni/coupons';
import { settlements } from '@alufie/payuni/settlements';
import { verifyWebhook } from '@alufie/payuni/webhooks';@alufie/payuni/operations exports the complete operation registry and
defineOperation() for a documented PAYUNi extension or an upstream ambiguity.
Custom operation paths must be root-relative and always resolve against the
selected official PAYUNi origin. Built-in contracts are deeply immutable. The
client validates unknown fields instead of silently submitting misspellings.
Settings form metadata
Settings metadata contains definitions only, never credential values:
import { Settings } from '@alufie/payuni/settings';
const fullForm = Settings.all;
const paymentForm = Settings.forFeatures(['payments']);
const credentialsOnly = Settings.pick(Settings.core, [
'environment',
'merchantId',
'hashKey',
'hashIv'
]);
for (const field of paymentForm.fields) {
// Map valueType, title, constraints, and sensitive to your own components.
}
const issues = Settings.validate(credentialsOnly, submittedValues);This is intentionally not tied to shadcn, Svelte, React, or any database.
Logs
import { createLogger } from '@alufie/payuni';
const logger = createLogger(async (event) => {
await database.insertPayuniLog(event);
});Hooks receive lifecycle metadata only. Decrypted payloads, card data, credentials, tokens, customer contact details, and raw callbacks are never passed to the logger. Storage, retention, and UI rendering remain site-owned because PAYUNi does not document a provider log-retention contract.
Webhooks
import {
createWebhookResponse,
webhookSchemas,
verifyWebhook
} from '@alufie/payuni/webhooks';
const verified = await verifyWebhook(urlSearchParams, {
credentials,
schema: webhookSchemas.payment
});
await database.transaction(async (tx) => {
await tx.insertWebhookOnce(verified.deduplicationKey, verified.data);
await tx.updateOrderFromVerifiedPayuniEvent(verified.data);
});
return createWebhookResponse();The verifier checks the hash before AES-GCM decryption, compares the resolved,
inner, and outer merchant IDs, rejects duplicate envelope fields, and can
validate the decrypted data with built-in payment, recurring, logistics, or
coupon schemas. verified.status and verified.message are outer PAYUNi
metadata; apply business transitions only from verified.data reconciled with
your stored order. Its deduplication key is a digest of the verified signed
envelope, so distinct callback payloads cannot collapse merely because their
transaction status matches. PAYUNi does not document an acknowledgement body,
automatic retry count, or retry backoff. createWebhookResponse() therefore
defaults to an empty HTTP 204; change it if PAYUNi gives your merchant a
different contract. Make processing idempotent and reconcile UNKNOWN through
the query API.
Successful direct API responses are checked against the operation's runtime
response schema. Raw response bodies are omitted by default. Only enable
exposeRawResponseBodies: true for short-lived diagnostics in a protected
environment; raw signed envelopes and upstream error bodies must not be logged.
Guides
Development
pnpm check
pnpm lint
pnpm test
pnpm build
npm pack --dry-runNo live PAYUNi request runs in the test suite. Live testing requires a merchant account, enabled payment tools, and (for several direct APIs) an approved source IP.
