$npx -y skills add glitternetwork/pinme --skill pinme-uniwebpayUse 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/
| 1 | # PinMe UniwebPay Payment Integration |
| 2 | |
| 3 | Guides writing payment services in a PinMe Worker (Cloudflare Worker TypeScript) that call UniwebPay directly through `@uniwebpay/sdk`. |
| 4 | |
| 5 | 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. |
| 6 | |
| 7 | ## Environment Binding Contract |
| 8 | |
| 9 | ```typescript |
| 10 | export interface Env { |
| 11 | UNIWEB_SECRET: string; // PinMe-provisioned sk_server_ key (server-side only) |
| 12 | UNIWEB_WEBHOOK_SECRET?: string; // wallet-level whsec_, used to verify webhook signatures |
| 13 | UNIWEB_API_URL?: string; // UniwebPay API endpoint override (default https://apiskill.uniwebpay.com) |
| 14 | UNIWEB_PAY_URL?: string; // UniwebPay checkout host override (default https://skill.uniwebpay.com) |
| 15 | UNIWEB_WALLET_ID?: string; // user-level wallet id (wal_), diagnostics/reconciliation only |
| 16 | WORKER_URL?: string; // this project's public URL: https://{projectName}.{platform api domain} |
| 17 | PROJECT_NAME?: string; // PinMe project name |
| 18 | DB?: D1Database; // project D1 (if enabled) |
| 19 | } |
| 20 | ``` |
| 21 | |
| 22 | Injection rules (metadata is rebuilt server-side by PinMe at deploy time; client-supplied bindings are ignored): |
| 23 | |
| 24 | - 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. |
| 25 | - `WORKER_URL`, `PROJECT_NAME`, `API_KEY`, `DB` and other base bindings are injected on every deploy, independent of UniwebPay. |
| 26 | - All projects owned by the same PinMe user share one wallet, one `sk_server_`, and one `whsec_`. |
| 27 | - 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. |
| 28 | - 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. |
| 29 | |
| 30 | ## SDK Client |
| 31 | |
| 32 | Always instantiate on the server side (the Worker); the SDK throws when run in a browser: |
| 33 | |
| 34 | ```typescript |
| 35 | import Uniweb from "@uniwebpay/sdk"; |
| 36 | |
| 37 | function uniwebClient(env: Env): Uniweb { |
| 38 | return new Uniweb(env.UNIWEB_SECRET, { |
| 39 | baseUrl: env.UNIWEB_API_URL, |
| 40 | payUrl: env.UNIWEB_PAY_URL, |
| 41 | }); |
| 42 | } |
| 43 | ``` |
| 44 | |
| 45 | - 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) }`. |
| 46 | - The SDK auto-retries only GET/DELETE on 429/5xx; POST/PATCH are never retried (avoids duplicate charges). |
| 47 | - Install `@uniwebpay/sdk` only when Worker code imports it; pick the package manager from the project's existing lockfile. |
| 48 | |
| 49 | ## Choosing an Integration Path |
| 50 | |
| 51 | | Scenario | Approach | Returns | |
| 52 | |------|------|------| |
| 53 | | Fixed-amount one-time collection | `uniweb.links.create(...)` | Permanent, reusable `/p/` link (one-time payments only) | |
| 54 | | Stable product catalog | `products.create` + `prices.create` once, store the `priceId` | Price carries a permanent `paymentUrl` (`/buy/` link) | |
| 55 | | Dynamic cart/order | Reuse or create a price, then `uniweb.checkout.create(...)` | `session.url` — **one-time, expires in 24 hours** | |
| 56 | | Subscriptions | Recurring price + `checkout.create({ mode: "subscription" })` or `subscriptions.create` | Same as above | |
| 57 | | Server-side payment status checks | `payments.get / list` | Server routes only | |
| 58 | |
| 59 | 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`. |
| 60 | |
| 61 | ## Payment Methods and Currency Rules |
| 62 | |
| 63 | | Method | Supported currencies | |
| 64 | |------|---------| |
| 65 | | `card` | SGD, USD, EUR, GBP, JPY, CNY, HKD, AUD, MYR, THB (minimum 10 minor units) | |
| 66 | | `wechat` | SGD only | |
| 67 | | `alipay` | SGD only | |
| 68 | | `paynow` | SGD only | |
| 69 | |
| 70 | - The QR methods (wechat/alipay/paynow) **all support SGD only** — never generate "CNY via WeChat/Alipay" code. |
| 71 | - Subscriptions (recurring / `mode: "subscription"`) use `card` only. |
| 72 | - When `paymentMethodTypes` is omitted, the server picks sensible defaults for the currency; when passed explicitly, validate user input against the table above first. |
| 73 | |
| 74 | ## SDK Surface Quick R |