glitternetwork/pinme/pinme-uniwebpay

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Use when generating, modifying, or reviewing PinMe Worker (Cloudflare Worker TypeScript) code that accepts payments through UniwebPay — payment links, products/prices, checkout sessions, payment status reads, refunds, subscriptions, or handling UniwebPay webhooks with @uniwebpay/sdk in a PinMe project.

Install

npx vskill@latest install glitternetwork/pinme/pinme-uniwebpay

SKILL.md

PinMe UniwebPay Payment Integration

Guides writing payment services in a PinMe Worker (Cloudflare Worker TypeScript) that call UniwebPay directly through @uniwebpay/sdk.

Core model: PinMe provisions the UniwebPay wallet and keys per PinMe user (not per project) and injects UNIWEB_* environment bindings at Worker deploy time; Worker code calls UniwebPay directly with the SDK — it does not go through PinMe payment proxy routes, and it must not call the legacy VibeCash APIs.

Environment Binding Contract

typescript
export interface Env {
UNIWEB_SECRET: string; // PinMe-provisioned sk_server_ key (server-side only)
UNIWEB_WEBHOOK_SECRET?: string; // wallet-level whsec_, used to verify webhook signatures
UNIWEB_API_URL?: string; // UniwebPay API endpoint override (default https://apiskill.uniwebpay.com)
UNIWEB_PAY_URL?: string; // UniwebPay checkout host override (default https://skill.uniwebpay.com)
UNIWEB_WALLET_ID?: string; // user-level wallet id (wal_), diagnostics/reconciliation only
WORKER_URL?: string; // this project's public URL: https://{projectName}.{platform api domain}
PROJECT_NAME?: string; // PinMe project name
DB?: D1Database; // project D1 (if enabled)
}

Injection rules (metadata is rebuilt server-side by PinMe at deploy time; client-supplied bindings are ignored):

  • The UNIWEB_* bindings are injected only after the user's UniwebPay credentials have been provisioned. Newly created projects are provisioned automatically and get them immediately; existing projects must be redeployed after enabling UniwebPay or rotating keys to pick up new bindings.
  • WORKER_URL, PROJECT_NAME, API_KEY, DB and other base bindings are injected on every deploy, independent of UniwebPay.
  • All projects owned by the same PinMe user share one wallet, one sk_server_, and one whsec_.
  • PinMe never gives the full wallet secret (sk_live_) to a Worker. Do not ask the user for it, and do not put it in code, wrangler.toml, .dev.vars, responses, logs, D1, or frontend bundles.
  • If UNIWEB_SECRET is missing at runtime, the user has not enabled UniwebPay or has not redeployed — tell the user to enable it and redeploy; never fabricate a value.

SDK Client

Always instantiate on the server side (the Worker); the SDK throws when run in a browser:

typescript
import Uniweb from "@uniwebpay/sdk";
function uniwebClient(env: Env): Uniweb {
return new Uniweb(env.UNIWEB_SECRET, {
baseUrl: env.UNIWEB_API_URL,
payUrl: env.UNIWEB_PAY_URL,
});
}
  • The constructor's first positional argument is the key (must have an sk_server_ or sk_live_ prefix); the second is optional options: { baseUrl?, payUrl?, timeout? (default 30s), maxRetries? (default 2) }.
  • The SDK auto-retries only GET/DELETE on 429/5xx; POST/PATCH are never retried (avoids duplicate charges).
  • Install @uniwebpay/sdk only when Worker code imports it; pick the package manager from the project's existing lockfile.

Choosing an Integration Path

ScenarioApproachReturns
Fixed-amount one-time collectionuniweb.links.create(...)Permanent, reusable /p/ link (one-time payments only)
Stable product catalogproducts.create + prices.create once, store the priceIdPrice carries a permanent paymentUrl (/buy/ link)
Dynamic cart/orderReuse or create a price, then uniweb.checkout.create(...)session.url — one-time, expires in 24 hours
SubscriptionsRecurring price + checkout.create({ mode: "subscription" }) or subscriptions.createSame as above
Server-side payment status checkspayments.get / listServer routes only

Amounts are always integer minor units (cents). Default currency convention is SGD unless the app has a stronger existing convention. Do not create a new product/price on every page view — create stable catalog items once and persist the priceId.

Payment Methods and Currency Rules

MethodSupported currencies
cardSGD, USD, EUR, GBP, JPY, CNY, HKD, AUD, MYR, THB (minimum 10 minor units)
wechatSGD only
alipaySGD only
paynowSGD only
  • The QR methods (wechat/alipay/paynow) all support SGD only — never generate "CNY via WeChat/Alipay" code.
  • Subscriptions (recurring / mode: "subscription") use card only.
  • When paymentMethodTypes is omitted, the server picks sensible defaults for the currency; when passed explicitly, validate user input against the table above first.

SDK Surface Quick Reference

The surface below is verified against source. All parameter fields are camelCase (priceId, webhookUrl, startingAfter, …); the SDK handles wire-level conversion itself. list() returns { data: T[], hasMore: boolean }; listAll() is an async generator available on products, prices, payments, customers, subscriptions, and links (not on checkout or refunds).

Products (webhookUrl is the per-product callback override):

typescript
await uniweb.products.create({ name, description?, webhookUrl?, metadata? });
await uniweb.products.list({ limit?, startingAfter? });
await uniweb.products.get(productId);
await uniweb.products.update(productId, { name?, description?, webhookUrl?, active?, metadata? });
await uniweb.products.del(productId);
for await (const product of uniweb.products.listAll()) {}

Prices (the returned price carries a permanent paymentUrl; deactivate takes it off sale):

typescript
await uniweb.prices.create({
productId,
amount, // integer minor units
currency, // e.g. "SGD"
type, // "one_time" | "recurring"
interval?, // "day" | "week" | "month" | "year"; recurring only
intervalCount?,
trialPeriodDays?,
metadata?,
});
await uniweb.prices.list({ productId?, limit?, startingAfter? });
await uniweb.prices.get(priceId);
await uniweb.prices.update(priceId, { active });
await uniweb.prices.activate(priceId);
await uniweb.prices.deactivate(priceId);
for await (const price of uniweb.prices.listAll({ productId? })) {}

Checkout sessions (do not accept `webhookUrl` — events resolve through the price → product → wallet chain; the URL is one-time and expires after 24 hours):

typescript
await uniweb.checkout.create({
mode, // "payment" | "subscription"
lineItems: [{ priceId, quantity }],
successUrl?,
cancelUrl?,
customerEmail?,
customerId?,
trialPeriodDays?,
paymentMethodTypes?, // ["card", "wechat", "alipay", "paynow"]
metadata?,
});
await uniweb.checkout.list({ limit?, startingAfter? });
await uniweb.checkout.get(checkoutSessionId);

Payments (for server-side status checks; only mark a local order paid when amount, currency, metadata, and order state all match expectations):

typescript
await uniweb.payments.create({ amount, currency, customerId?, metadata? });
await uniweb.payments.list({ status?, customerId?, limit?, startingAfter? });
await uniweb.payments.get(paymentId, { gateway? });
await uniweb.payments.listRefunds(paymentId);
await uniweb.payments.sync(paymentId);
await uniweb.payments.void(paymentId);
for await (const payment of uniweb.payments.listAll({ status?, customerId? })) {}

Refunds (no list/listAll — use payments.listRefunds):

typescript
await uniweb.refunds.create({ paymentId, amount?, reason?, offlineRefundFlag? });
await uniweb.refunds.get(refundId, { gateway? });

Customers:

typescript
await uniweb.customers.create({ email, name?, metadata? });
await uniweb.customers.list({ email?, limit?, startingAfter? });
await uniweb.customers.get(customerId);
await uniweb.customers.update(customerId, { email?, name?, metadata? });
await uniweb.customers.del(customerId);
for await (const customer of uniweb.customers.listAll({ email? })) {}

Subscriptions (states include trialing / active / past_due / unpaid / canceled; update access only from verified webhooks or a trusted server-side reconciliation job):

typescript
await uniweb.subscriptions.create({ customerId, priceId, paymentMethodId?, trialPeriodDays?, metadata? });
await uniweb.subscriptions.list({ customerId?, status?, limit?, startingAfter? });
await uniweb.subscriptions.get(subscriptionId);
await uniweb.subscriptions.update(subscriptionId, { cancelAtPeriodEnd? });
await uniweb.subscriptions.cancel(subscriptionId); // cancel immediately
await uniweb.subscriptions.resume(subscriptionId); // undo cancelAtPeriodEnd
for await (const subscription of uniweb.subscriptions.listAll({ customerId?, status? })) {}

Payment links (permanent reusable /p/ links, one-time collection only; webhookUrl is the per-link callback override):

typescript
await uniweb.links.create({
amount,
currency,
name?,
description?,
successUrl?,
cancelUrl?,
webhookUrl?,
paymentMethodTypes?,
metadata?,
});
await uniweb.links.list({ limit?, startingAfter? });
await uniweb.links.get(paymentLinkId);
await uniweb.links.update(paymentLinkId, { name?, description?, successUrl?, cancelUrl?, webhookUrl?, active? });
await uniweb.links.deactivate(paymentLinkId);
for await (const link of uniweb.links.listAll()) {}

Wallet and wallet-level webhook configuration (danger zone: affects the wallet shared by ALL of the user's projects):

typescript
await uniweb.wallet.current();
await uniweb.wallet.update({ merchantName?, merchantCity?, merchantCountry?, webhookUrl? });
await uniweb.webhooks.set(url); // overwrites the wallet-level callback URL
await uniweb.webhooks.info();
await uniweb.webhooks.remove();
await uniweb.webhooks.rollSecret(); // rotates the shared whsec_

Ordinary project routes must not call webhooks.set / remove / rollSecret or wallet.update — they mutate the wallet callback fallback and signing secret shared across all of the user's projects. Generate them only when the user explicitly asks for wallet administration and the route has project/admin-level authorization. Same for refunds, subscription mutations, payouts, KYC, and bank account APIs: generate only when the user explicitly requests that business flow and the code has validation, persistence, and authorization.

Webhook Integration

Callback URL: set it on the link/product, pointing at this Worker

Event delivery precedence: per-link `webhookUrl` > per-product `webhookUrl` > wallet-level fallback. The signing secret is always the wallet-level whsec_ (i.e. env.UNIWEB_WEBHOOK_SECRET).

PinMe sets a managed fallback callback URL on the wallet, but it exists only to obtain and preserve the signing secret — PinMe's server discards events it receives there (204); it never forwards them to the Worker. Business events must therefore set this project's webhookUrl explicitly on the resource that creates the payment:

  • Payment links: pass webhookUrl on links.create.
  • Checkout sessions: checkout.create has no webhookUrl field; events route through the price's product — set webhookUrl on products.create (or on the reused product).

Rules for building the webhookUrl:

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

7-day trend score
0

Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/glitternetwork/pinme/pinme-uniwebpay/badge)](https://verified-skill.com/skills/glitternetwork/pinme/pinme-uniwebpay)

Something wrong with this skill or its page? Tell us.