@tradom/payment
v0.2.1
Published
Official TypeScript / Node.js client for the TRADOM Payment (T-Pay) API.
Maintainers
Readme
@tradom/payment
Official TypeScript / Node.js client for the TRADOM Payment (T-Pay) API.
Covers the full merchant surface of the platform:
| Resource | API | Key |
| --- | --- | --- |
| client.gateway | EC Gateway — redirect-based hosted checkout (POST /api/gateway/orders, status reconciliation) | gw_live_… |
| client.paymentIntents | Stripe-style PaymentIntent API (/api/v1/payment_intents…) — create / quote / confirm / cancel / refund / sandbox deposit | sk_test_… / sk_live_… |
| client.webhookEndpoints | Webhook receiver registration (/api/v1/webhook_endpoints) | sk_… |
| client.events | Poll-based event access (/api/v1/events) | sk_… |
| verifyWebhookSignature / constructEvent | Verify signed webhook deliveries (Tradom-Signature) | whsec_… |
⚠️ Server-side only. Every key this SDK takes is a secret. Never ship it to the browser — call the SDK from your backend / BFF.
The two key families are separate credentials: the EC Gateway uses your merchant gateway key (
gw_live_…); the PaymentIntent API uses a secret key (sk_test_…/sk_live_…). Construct onePaymentClientper key.
Install
npm install @tradom/paymentWorks with any package manager (pnpm add / yarn add / bun add @tradom/payment).
Quick start — EC Gateway (redirect flow)
Your EC backend sends the order, TRADOM runs KYC and hosts the entire payment UI; you redirect the buyer and reconcile server-side.
import { PaymentClient } from "@tradom/payment";
const tpay = new PaymentClient({
apiKey: process.env.TPAY_GATEWAY_KEY!, // gw_live_...
// baseUrl defaults to https://payment.tradom.jp
});
const order = await tpay.gateway.createOrder({
orderId: "order_12345", // your id — idempotent per merchant
amount: 50000, // JPY, integer > 0
buyerName: "Taro Yamada",
buyerEmail: "[email protected]",
country: "JP",
items: [{ name: "Coffee beans", category: "food" }],
returnUrl: "https://shop.example/checkout/complete",
cancelUrl: "https://shop.example/checkout/cancel",
});
// Discriminated union — a declined KYC is a normal result, not an error.
redirect(order.checkoutUrl); // "ok" → hosted checkout / "kyc_declined" → decline pageReconcile before fulfilment — never trust the browser redirect alone:
// token = the path segment of order.checkoutUrl (…/gateway/checkout/<token>/)
const status = await tpay.gateway.checkoutStatus(token);
if (status.confirmed) fulfil(); // or gate on status.state.state >= "deposit_detected"
console.log(status.state.state, status.message?.titleJa);Quick start — PaymentIntent API (server-driven flow)
const tpay = new PaymentClient({ apiKey: process.env.TPAY_SECRET_KEY! }); // sk_test_...
// 1) Create from a payment link (idempotent via Idempotency-Key)
const pi = await tpay.paymentIntents.create({
paymentLinkToken: "<share_token>",
metadata: { order: "42" },
idempotencyKey: "order-42",
});
// 2) Optional preview
const quote = await tpay.paymentIntents.cryptoQuote(pi.id, {
stablecoin: "usdc",
network: "ethereum",
});
// 3) Lock the rail → deposit instructions (15-minute window)
const confirmed = await tpay.paymentIntents.confirm(pi.id, {
stablecoin: "usdc",
network: "ethereum",
});
const deposit = confirmed.nextAction!.cryptoDeposit;
// deposit.depositAddress / deposit.amount / deposit.eip681Uri / deposit.expiresAt
// 4) [sandbox] simulate the on-chain deposit → "succeeded"
await tpay.paymentIntents.simulateDeposit(pi.id);
// 5) Refund (fully, or partially with { amount })
const { refund } = await tpay.paymentIntents.refund(pi.id, { amount: 50 });Amounts are major fiat units (USD dollars / JPY yen) — not cents. Don't multiply by 100.
Webhooks
Register a receiver once, store the one-time secret, then verify every delivery:
const ep = await tpay.webhookEndpoints.create({ url: "https://shop.example/tpay-webhook" });
save(ep.secret); // whsec_… — shown only at creationimport { constructEvent, PaymentError } from "@tradom/payment";
// Next.js route handler — use the RAW body, not a re-serialised object.
export async function POST(request: Request) {
let event;
try {
event = constructEvent({
payload: await request.text(),
signature: request.headers.get("Tradom-Signature") ?? "",
secret: process.env.TPAY_WEBHOOK_SECRET!,
});
} catch (e) {
if (e instanceof PaymentError) return new Response("bad signature", { status: 400 });
throw e;
}
// At-least-once delivery — dedupe on event.id.
if (event.type === "payment_intent.succeeded") {
fulfil(event.data.object.metadata.order);
}
return new Response("ok");
}Emitted event types: payment_intent.succeeded · payment_intent.canceled ·
payment_intent.refunded (there is no payment_intent.created).
Deliveries retry every 10 minutes for up to 72h / 5 attempts; use
client.events.list() as a catch-up mechanism.
API surface
new PaymentClient(options)
| Option | Type | Default | Notes |
| ------------ | ------------------------- | --------------------------- | ------------------------------------------------------------ |
| apiKey | string | — | Required. gw_live_… (gateway) or sk_… (PaymentIntent). |
| baseUrl | string | https://payment.tradom.jp | Override to target a non-production environment. |
| authScheme | "x-api-key" \| "bearer" | "x-api-key" | Gateway only — /api/v1 always uses Authorization: Bearer. |
| timeoutMs | number | 15000 | Per-request timeout. |
| fetch | FetchLike | globalThis.fetch | Inject for testing / custom agents. |
client.gateway
createOrder(params): Promise<OrderApproved | OrderDeclined>— see the field mapping below. Idempotent per(merchant, orderId).checkoutStatus(token): Promise<CheckoutStatus>— public token-scoped reconciliation:confirmed,tosAccepted, canonicalstate(12 states,state.isTerminal,state.matchStatus), bilingual buyermessage(M-code). ThrowsPaymentError("not_found", 404)for unknown tokens.
| createOrder field | Required | Maps to |
| ------------------- | -------- | ------------- |
| orderId | ✅ | order_id |
| amount | ✅ | amount (JPY)|
| buyerName | | buyer_name |
| buyerEmail | | buyer_email |
| country | | country |
| items | | items |
| returnUrl | | return_url |
| cancelUrl | | cancel_url |
client.paymentIntents
create({ paymentLinkToken, mode?, metadata?, idempotencyKey? })createCheckoutSession({ paymentLinkToken, mode?, metadata? })— keyless hosted-checkout entry (returnsclientSecret)retrieve(id)/cryptoQuote(id, { stablecoin, network })confirm(id, { stablecoin, network })— testnet mode supports USDC onlycancel(id)/refund(id, { amount?, reason? })/simulateDeposit(id, { amount? })
All calls resolve to a fully camelCased PaymentIntent (statuses:
requires_payment_method → requires_action → succeeded, plus canceled).
Webhook helpers (standalone exports)
verifyWebhookSignature({ payload, signature, secret, toleranceSeconds?, now? }): booleanconstructEvent(sameParams): WebhookEvent— verify + parse, throws on mismatch
Signature scheme: Tradom-Signature: t=<unix>,v1=<hex hmac-sha256(secret, "<t>.<payload>")>,
default freshness tolerance 300s, constant-time comparison. Requires Node ≥ 18 / Bun
(node:crypto).
Errors
Transport problems, local validation failures, and API error envelopes all
throw a PaymentError with code, httpStatus (0 for local/network) and,
for PaymentIntent API errors, param:
| Source | Envelope | Example codes |
| --- | --- | --- |
| EC Gateway | {"status":"error","error":"…"} | missing_api_key (401) · invalid_api_key (403) · invalid_json · service messages like "amount must be greater than zero" · session_expired (409) |
| PaymentIntent API | {"error":{"code","message","param"?}} | authentication_required (401) · parameter_missing / parameter_invalid (400) · resource_missing (404) · payment_link_inactive (409) · crypto_asset_unsupported (400) · payment_intent_unexpected_state (409) · url_invalid (400) |
| Client-side | — | invalid_request · network_error · timeout · not_found · bad_response · signature_verification_failed |
Note: PaymentIntent API message strings are mixed Japanese/English — always
branch on code, never on message.
Development
Tests run on Bun (bun:test):
bun install
bun test # mocked fetch + webhook signature vectors
bun run typecheck
bun run build # dist/ (ESM + d.ts)