@siglume/direct-request-payment 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -50
- package/LICENSE +21 -21
- package/README.md +424 -404
- package/dist/index.cjs +6 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -6
- package/dist/index.d.ts +9 -6
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/docs/announcement-ja.md +81 -64
- package/docs/api-reference.md +335 -274
- package/docs/merchant-quickstart.md +302 -296
- package/docs/pricing.md +93 -73
- package/docs/security.md +114 -99
- package/examples/express-checkout.ts +106 -106
- package/examples/setup-merchant.ts +17 -17
- package/package.json +71 -71
package/docs/pricing.md
CHANGED
|
@@ -1,73 +1,93 @@
|
|
|
1
|
-
# Pricing
|
|
2
|
-
|
|
3
|
-
This page documents the trial-phase merchant pricing for Siglume Direct Request
|
|
4
|
-
Payment as of 2026-06-
|
|
5
|
-
release; the Siglume platform response is the source of truth for per-payment
|
|
6
|
-
fee data returned at runtime.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
1
|
+
# Pricing
|
|
2
|
+
|
|
3
|
+
This page documents the trial-phase merchant pricing for Siglume Direct Request
|
|
4
|
+
Payment as of 2026-06-18. Pricing can change by agreement or future product
|
|
5
|
+
release; the Siglume platform response is the source of truth for per-payment
|
|
6
|
+
fee data returned at runtime.
|
|
7
|
+
|
|
8
|
+
These prices apply to **SDRP Standard Payment** through the external merchant
|
|
9
|
+
Direct Request Payment SDK. They do not apply to API Store Micro Payment or Nano
|
|
10
|
+
Payment usage events.
|
|
11
|
+
|
|
12
|
+
## Settlement Currencies
|
|
13
|
+
|
|
14
|
+
Siglume Direct Request Payment launches in the US and Japan, and both settlement
|
|
15
|
+
currencies are first-class:
|
|
16
|
+
|
|
17
|
+
- **JPY**, settled on-chain in **JPYC**
|
|
18
|
+
- **USD**, settled on-chain in **USDC**
|
|
19
|
+
|
|
20
|
+
A merchant settles in a single currency, chosen at onboarding. The settlement fee
|
|
21
|
+
percentage (the payment fee column below) is identical in both currencies. Only
|
|
22
|
+
the flat amounts — the monthly base fee and the per-payment minimum fee — are
|
|
23
|
+
quoted per currency.
|
|
24
|
+
|
|
25
|
+
## SDRP Menu Boundary
|
|
26
|
+
|
|
27
|
+
SDRP is the protocol family. This SDK is for external-merchant Standard Payment:
|
|
28
|
+
one buyer-approved payment, subscription charge, or scheduled-autopay occurrence
|
|
29
|
+
settles through DirectPaymentHub.
|
|
30
|
+
|
|
31
|
+
| SDRP menu | Amount band | Fee model | Settlement cadence |
|
|
32
|
+
| --- | --- | --- | --- |
|
|
33
|
+
| Standard Payment | Over JPY 500 / over USD 3.00, or immediate finality required | Merchant plan percentage with JPY 30 / USD 0.20 minimum | Immediate ordinary wallet settlement |
|
|
34
|
+
| Micro Payment | JPY 50-500 / about USD 0.30-3.00 | USD 0.01 / accepted Tx, about JPY 2 | API Store internal meter, weekly delayed settlement |
|
|
35
|
+
| Nano Payment | Under JPY 1 to JPY 49 / under USD 0.01 to about USD 0.30 | USD 0.001 / accepted usage, about JPY 0.2 | API Store internal meter, monthly delayed settlement |
|
|
36
|
+
|
|
37
|
+
For Micro Payment and Nano Payment, the API Store meter gate runs before provider
|
|
38
|
+
execution. Budget or scope failures are recorded as `rejected_no_charge`; the
|
|
39
|
+
provider API is not called and no pending provider revenue is created.
|
|
40
|
+
|
|
41
|
+
## Trial Plans
|
|
42
|
+
|
|
43
|
+
| Plan | Monthly fee (JPY) | Monthly fee (USD) | Payment fee | Intended starting point |
|
|
44
|
+
| --- | ---: | ---: | ---: | --- |
|
|
45
|
+
| Launch | JPY 0 | USD 0 | 1.8% | Proofs of concept and low-volume trials |
|
|
46
|
+
| Starter | JPY 980 | USD 6.00 | 1.0% | Early production checkout trials |
|
|
47
|
+
| Growth | JPY 2,980 | USD 18.00 | 0.7% | Growing EC, booking, membership, and API services |
|
|
48
|
+
| Pro | JPY 9,800 | USD 60.00 | 0.5% | Higher-volume merchant integrations |
|
|
49
|
+
|
|
50
|
+
Every payment is fee-bearing at the plan rate. The minimum fee is JPY 30
|
|
51
|
+
(USD merchants: USD 0.20) per payment. The minimum covers the worst-case
|
|
52
|
+
per-payment settlement cost (an on-chain signature plus network gas), so small
|
|
53
|
+
payments are never processed at a loss; on larger payments the percentage rate
|
|
54
|
+
applies instead.
|
|
55
|
+
|
|
56
|
+
USD pricing is the JPY tier converted at roughly 160 JPY/USD and rounded to
|
|
57
|
+
clean price points that keep the same 1:3:10 tier ratio.
|
|
58
|
+
|
|
59
|
+
If no paid plan is selected during merchant setup, the merchant account uses the
|
|
60
|
+
Launch plan. A merchant billing mandate is still required before accepting
|
|
61
|
+
payments so Siglume can collect the monthly base fee automatically.
|
|
62
|
+
|
|
63
|
+
The current Siglume API and merchant registry may still expose the internal
|
|
64
|
+
`billing_plan` value `free` for the Launch tier. Treat `free` as an internal
|
|
65
|
+
compatibility key, not the public plan name. (Until 2026-06-12 the Launch plan
|
|
66
|
+
included a free monthly allowance of 100 payments; that allowance has been
|
|
67
|
+
retired — the platform `fee_bps` response is always the source of truth.)
|
|
68
|
+
|
|
69
|
+
Per-payment fees are collected during payment settlement through the
|
|
70
|
+
DirectPaymentHub split. The merchant receives the net amount after that fee.
|
|
71
|
+
Monthly base fees are collected separately through the merchant billing mandate.
|
|
72
|
+
|
|
73
|
+
The same fee schedule applies in JPY and USD. The Siglume platform returns
|
|
74
|
+
`fee_bps` in the merchant's settlement currency on every payment requirement, so
|
|
75
|
+
the SDK never has to know which currency table to read — it just trusts the
|
|
76
|
+
value Siglume returns.
|
|
77
|
+
|
|
78
|
+
## SDK Behavior
|
|
79
|
+
|
|
80
|
+
The SDK does not calculate merchant invoices or enforce plan limits locally.
|
|
81
|
+
Instead, it exposes billing-related values returned by Siglume, including
|
|
82
|
+
`fee_bps` on a payment requirement. This keeps merchant billing centralized in
|
|
83
|
+
the Siglume platform and avoids stale client-side pricing logic.
|
|
84
|
+
|
|
85
|
+
## Supported Use Cases
|
|
86
|
+
|
|
87
|
+
The trial pricing is intended for:
|
|
88
|
+
|
|
89
|
+
- Small EC checkout
|
|
90
|
+
- Booking and reservation services
|
|
91
|
+
- Membership services
|
|
92
|
+
- Paid API access
|
|
93
|
+
- Scheduled autopay for external merchant workflows
|
package/docs/security.md
CHANGED
|
@@ -1,99 +1,114 @@
|
|
|
1
|
-
# Security Guide
|
|
2
|
-
|
|
3
|
-
Direct Request Payment is a wallet payment rail. Treat it like payment
|
|
4
|
-
infrastructure, not like a generic API call.
|
|
5
|
-
|
|
6
|
-
## Do Not Expose Secrets
|
|
7
|
-
|
|
8
|
-
These values must stay server-side:
|
|
9
|
-
|
|
10
|
-
- `SIGLUME_DIRECT_PAYMENT_CHALLENGE_SECRET`
|
|
11
|
-
- `SIGLUME_WEBHOOK_SECRET`
|
|
12
|
-
- any merchant administrative credentials
|
|
13
|
-
|
|
14
|
-
The buyer-facing browser may receive the signed `challenge` string, but never
|
|
15
|
-
the secret that produced it.
|
|
16
|
-
|
|
17
|
-
## Keep JWT Roles Separate
|
|
18
|
-
|
|
19
|
-
Use the merchant's Siglume JWT only for setup actions such as `setupCheckout`,
|
|
20
|
-
challenge secret rotation, billing mandate preparation, and webhook subscription
|
|
21
|
-
creation.
|
|
22
|
-
|
|
23
|
-
Use the buyer's Siglume JWT only when creating and paying a payment requirement.
|
|
24
|
-
A merchant JWT or Developer Portal `cli_` key must not be used to charge a
|
|
25
|
-
customer wallet.
|
|
26
|
-
|
|
27
|
-
## Bind the Order Server-Side
|
|
28
|
-
|
|
29
|
-
The HMAC challenge covers:
|
|
30
|
-
|
|
31
|
-
```text
|
|
32
|
-
merchant:amount_minor:currency:nonce
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Use a nonce derived from a durable order payment attempt, for example
|
|
36
|
-
`order_123-attempt_1`. The nonce must not contain `:` because the platform
|
|
37
|
-
challenge is encoded as `scheme:nonce:signature`. Store the returned
|
|
38
|
-
`challenge_hash` on the order. When a
|
|
39
|
-
webhook arrives, look up the order by `challenge_hash`.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
1
|
+
# Security Guide
|
|
2
|
+
|
|
3
|
+
Direct Request Payment is a wallet payment rail. Treat it like payment
|
|
4
|
+
infrastructure, not like a generic API call.
|
|
5
|
+
|
|
6
|
+
## Do Not Expose Secrets
|
|
7
|
+
|
|
8
|
+
These values must stay server-side:
|
|
9
|
+
|
|
10
|
+
- `SIGLUME_DIRECT_PAYMENT_CHALLENGE_SECRET`
|
|
11
|
+
- `SIGLUME_WEBHOOK_SECRET`
|
|
12
|
+
- any merchant administrative credentials
|
|
13
|
+
|
|
14
|
+
The buyer-facing browser may receive the signed `challenge` string, but never
|
|
15
|
+
the secret that produced it.
|
|
16
|
+
|
|
17
|
+
## Keep JWT Roles Separate
|
|
18
|
+
|
|
19
|
+
Use the merchant's Siglume JWT only for setup actions such as `setupCheckout`,
|
|
20
|
+
challenge secret rotation, billing mandate preparation, and webhook subscription
|
|
21
|
+
creation.
|
|
22
|
+
|
|
23
|
+
Use the buyer's Siglume JWT only when creating and paying a payment requirement.
|
|
24
|
+
A merchant JWT or Developer Portal `cli_` key must not be used to charge a
|
|
25
|
+
customer wallet.
|
|
26
|
+
|
|
27
|
+
## Bind the Order Server-Side
|
|
28
|
+
|
|
29
|
+
The HMAC challenge covers:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
merchant:amount_minor:currency:nonce
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Use a nonce derived from a durable order payment attempt, for example
|
|
36
|
+
`order_123-attempt_1`. The nonce must not contain `:` because the platform
|
|
37
|
+
challenge is encoded as `scheme:nonce:signature`. Store the returned
|
|
38
|
+
`challenge_hash` on the order. When a
|
|
39
|
+
webhook arrives, look up the order by `challenge_hash`.
|
|
40
|
+
|
|
41
|
+
Recurring approvals use a different challenge scheme and HMAC material:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
merchant:amount_minor:currency:cadence:nonce
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`cadence="monthly"` is for subscriptions. `cadence="daily"` is the scheduled
|
|
48
|
+
autopay approval tag; it does not itself limit occurrences to once per day.
|
|
49
|
+
Scheduled autopay execution is bounded by the buyer-approved per-run, daily, and
|
|
50
|
+
monthly auto-pay budget.
|
|
51
|
+
|
|
52
|
+
## Do Not Trust Browser Amounts
|
|
53
|
+
|
|
54
|
+
The merchant server owns:
|
|
55
|
+
|
|
56
|
+
- SKU or plan
|
|
57
|
+
- amount in minor units
|
|
58
|
+
- currency
|
|
59
|
+
- nonce
|
|
60
|
+
|
|
61
|
+
If a browser says the order total is 1200 JPY, treat that as display state only.
|
|
62
|
+
Re-read the order server-side before generating the challenge.
|
|
63
|
+
|
|
64
|
+
## Webhook Verification
|
|
65
|
+
|
|
66
|
+
Verify the `Siglume-Signature` header using the raw request body. Do not parse
|
|
67
|
+
and re-stringify JSON before verification.
|
|
68
|
+
|
|
69
|
+
The SDK expects the Siglume signature format:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
t=<unix timestamp>,v1=<hex hmac sha256>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The signed payload is:
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
<timestamp>.<raw body>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The default tolerance is 300 seconds.
|
|
82
|
+
|
|
83
|
+
Use verified webhook data as the durable completion signal. Browser redirects,
|
|
84
|
+
client-side callbacks, or local transaction responses can improve UX, but they
|
|
85
|
+
should not be the only source used to fulfill an order.
|
|
86
|
+
|
|
87
|
+
## Idempotency
|
|
88
|
+
|
|
89
|
+
Fulfill exactly once per order. Store at least:
|
|
90
|
+
|
|
91
|
+
- order id
|
|
92
|
+
- challenge hash
|
|
93
|
+
- Siglume requirement id
|
|
94
|
+
- on-chain receipt id or transaction hash if present
|
|
95
|
+
- fulfillment state
|
|
96
|
+
|
|
97
|
+
Duplicate webhook deliveries and manual redelivery can occur. A duplicate
|
|
98
|
+
webhook with the same requirement id must not ship the order twice.
|
|
99
|
+
|
|
100
|
+
## What Direct Request Payment Is Not
|
|
101
|
+
|
|
102
|
+
Direct Request Payment is not:
|
|
103
|
+
|
|
104
|
+
- stored value
|
|
105
|
+
- prepaid points
|
|
106
|
+
- escrow
|
|
107
|
+
- a platform balance
|
|
108
|
+
- a card payment fallback
|
|
109
|
+
- the API Store Micro Payment or Nano Payment meter
|
|
110
|
+
|
|
111
|
+
It is a Standard Payment wallet gate backed by an on-chain receipt. API Store
|
|
112
|
+
Micro Payment and Nano Payment are separate internal-meter, delayed-settlement
|
|
113
|
+
menus; they must fail closed before provider API execution when the buyer's
|
|
114
|
+
metered budget, scope, or amount band is invalid.
|
|
@@ -1,106 +1,106 @@
|
|
|
1
|
-
import express from "express";
|
|
2
|
-
import {
|
|
3
|
-
createDirectRequestPaymentChallenge,
|
|
4
|
-
DirectRequestPaymentClient,
|
|
5
|
-
verifyDirectRequestPaymentWebhook,
|
|
6
|
-
} from "@siglume/direct-request-payment";
|
|
7
|
-
|
|
8
|
-
const app = express();
|
|
9
|
-
const port = Number(process.env.PORT || 3000);
|
|
10
|
-
const merchantKey = process.env.SIGLUME_DIRECT_PAYMENT_MERCHANT || "example_merchant";
|
|
11
|
-
|
|
12
|
-
// Use JSON for normal routes. Use raw body only on the webhook route.
|
|
13
|
-
app.use((req, res, next) => {
|
|
14
|
-
if (req.path === "/siglume/webhook") {
|
|
15
|
-
next();
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
express.json()(req, res, next);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const orders = new Map<string, any>();
|
|
22
|
-
|
|
23
|
-
const asyncRoute =
|
|
24
|
-
(handler: express.RequestHandler): express.RequestHandler =>
|
|
25
|
-
(req, res, next) => {
|
|
26
|
-
Promise.resolve(handler(req, res, next)).catch(next);
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
app.post("/checkout/siglume/start", asyncRoute(async (req, res) => {
|
|
30
|
-
const orderId = String(req.body.order_id || "");
|
|
31
|
-
const order = orders.get(orderId);
|
|
32
|
-
if (!order) {
|
|
33
|
-
res.status(404).json({ error: "order_not_found" });
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
order.payment_attempt = Number(order.payment_attempt || 0) + 1;
|
|
38
|
-
const challenge = await createDirectRequestPaymentChallenge({
|
|
39
|
-
merchant: merchantKey,
|
|
40
|
-
amount_minor: order.amount_minor,
|
|
41
|
-
currency: order.currency,
|
|
42
|
-
secret: process.env.SIGLUME_DIRECT_PAYMENT_CHALLENGE_SECRET!,
|
|
43
|
-
nonce: `${order.id}-attempt_${order.payment_attempt}`,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
order.siglume_challenge_hash = challenge.challenge_hash;
|
|
47
|
-
order.siglume_payment_status = "pending";
|
|
48
|
-
|
|
49
|
-
res.json({
|
|
50
|
-
order_id: order.id,
|
|
51
|
-
amount_minor: order.amount_minor,
|
|
52
|
-
currency: order.currency,
|
|
53
|
-
siglume_challenge: challenge.challenge,
|
|
54
|
-
});
|
|
55
|
-
}));
|
|
56
|
-
|
|
57
|
-
app.post("/checkout/siglume/pay", asyncRoute(async (req, res) => {
|
|
58
|
-
const order = orders.get(String(req.body.order_id || ""));
|
|
59
|
-
if (!order) {
|
|
60
|
-
res.status(404).json({ error: "order_not_found" });
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// In production, obtain this from the authenticated buyer's Siglume session
|
|
65
|
-
// or a hosted Siglume payment confirmation flow. Do not use a merchant secret
|
|
66
|
-
// to charge a customer wallet.
|
|
67
|
-
const siglume = new DirectRequestPaymentClient({
|
|
68
|
-
auth_token: String(req.headers.authorization || "").replace(/^Bearer\s+/i, ""),
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const requirement = await siglume.createPaymentRequirement({
|
|
72
|
-
merchant: merchantKey,
|
|
73
|
-
amount_minor: order.amount_minor,
|
|
74
|
-
currency: order.currency,
|
|
75
|
-
challenge: String(req.body.siglume_challenge || ""),
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
res.json({ requirement });
|
|
79
|
-
}));
|
|
80
|
-
|
|
81
|
-
app.post("/siglume/webhook", express.raw({ type: "application/json" }), asyncRoute(async (req, res) => {
|
|
82
|
-
const header = String(req.headers["siglume-signature"] || "");
|
|
83
|
-
const { event } = await verifyDirectRequestPaymentWebhook(
|
|
84
|
-
process.env.SIGLUME_WEBHOOK_SECRET!,
|
|
85
|
-
req.body,
|
|
86
|
-
header,
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
if (event.type === "direct_payment.confirmed") {
|
|
90
|
-
const challengeHash = String(event.data.challenge_hash || "");
|
|
91
|
-
const order = [...orders.values()].find((item) => item.siglume_challenge_hash === challengeHash);
|
|
92
|
-
if (order) {
|
|
93
|
-
order.siglume_payment_status = "paid";
|
|
94
|
-
order.siglume_requirement_id = event.data.requirement_id || event.data.direct_payment_requirement_id;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
res.status(204).send();
|
|
99
|
-
}));
|
|
100
|
-
|
|
101
|
-
app.use((error: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
|
102
|
-
const message = error instanceof Error ? error.message : "internal_error";
|
|
103
|
-
res.status(500).json({ error: message });
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
app.listen(port);
|
|
1
|
+
import express from "express";
|
|
2
|
+
import {
|
|
3
|
+
createDirectRequestPaymentChallenge,
|
|
4
|
+
DirectRequestPaymentClient,
|
|
5
|
+
verifyDirectRequestPaymentWebhook,
|
|
6
|
+
} from "@siglume/direct-request-payment";
|
|
7
|
+
|
|
8
|
+
const app = express();
|
|
9
|
+
const port = Number(process.env.PORT || 3000);
|
|
10
|
+
const merchantKey = process.env.SIGLUME_DIRECT_PAYMENT_MERCHANT || "example_merchant";
|
|
11
|
+
|
|
12
|
+
// Use JSON for normal routes. Use raw body only on the webhook route.
|
|
13
|
+
app.use((req, res, next) => {
|
|
14
|
+
if (req.path === "/siglume/webhook") {
|
|
15
|
+
next();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
express.json()(req, res, next);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const orders = new Map<string, any>();
|
|
22
|
+
|
|
23
|
+
const asyncRoute =
|
|
24
|
+
(handler: express.RequestHandler): express.RequestHandler =>
|
|
25
|
+
(req, res, next) => {
|
|
26
|
+
Promise.resolve(handler(req, res, next)).catch(next);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
app.post("/checkout/siglume/start", asyncRoute(async (req, res) => {
|
|
30
|
+
const orderId = String(req.body.order_id || "");
|
|
31
|
+
const order = orders.get(orderId);
|
|
32
|
+
if (!order) {
|
|
33
|
+
res.status(404).json({ error: "order_not_found" });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
order.payment_attempt = Number(order.payment_attempt || 0) + 1;
|
|
38
|
+
const challenge = await createDirectRequestPaymentChallenge({
|
|
39
|
+
merchant: merchantKey,
|
|
40
|
+
amount_minor: order.amount_minor,
|
|
41
|
+
currency: order.currency,
|
|
42
|
+
secret: process.env.SIGLUME_DIRECT_PAYMENT_CHALLENGE_SECRET!,
|
|
43
|
+
nonce: `${order.id}-attempt_${order.payment_attempt}`,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
order.siglume_challenge_hash = challenge.challenge_hash;
|
|
47
|
+
order.siglume_payment_status = "pending";
|
|
48
|
+
|
|
49
|
+
res.json({
|
|
50
|
+
order_id: order.id,
|
|
51
|
+
amount_minor: order.amount_minor,
|
|
52
|
+
currency: order.currency,
|
|
53
|
+
siglume_challenge: challenge.challenge,
|
|
54
|
+
});
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
app.post("/checkout/siglume/pay", asyncRoute(async (req, res) => {
|
|
58
|
+
const order = orders.get(String(req.body.order_id || ""));
|
|
59
|
+
if (!order) {
|
|
60
|
+
res.status(404).json({ error: "order_not_found" });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// In production, obtain this from the authenticated buyer's Siglume session
|
|
65
|
+
// or a hosted Siglume payment confirmation flow. Do not use a merchant secret
|
|
66
|
+
// to charge a customer wallet.
|
|
67
|
+
const siglume = new DirectRequestPaymentClient({
|
|
68
|
+
auth_token: String(req.headers.authorization || "").replace(/^Bearer\s+/i, ""),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const requirement = await siglume.createPaymentRequirement({
|
|
72
|
+
merchant: merchantKey,
|
|
73
|
+
amount_minor: order.amount_minor,
|
|
74
|
+
currency: order.currency,
|
|
75
|
+
challenge: String(req.body.siglume_challenge || ""),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
res.json({ requirement });
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
app.post("/siglume/webhook", express.raw({ type: "application/json" }), asyncRoute(async (req, res) => {
|
|
82
|
+
const header = String(req.headers["siglume-signature"] || "");
|
|
83
|
+
const { event } = await verifyDirectRequestPaymentWebhook(
|
|
84
|
+
process.env.SIGLUME_WEBHOOK_SECRET!,
|
|
85
|
+
req.body,
|
|
86
|
+
header,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (event.type === "direct_payment.confirmed") {
|
|
90
|
+
const challengeHash = String(event.data.challenge_hash || "");
|
|
91
|
+
const order = [...orders.values()].find((item) => item.siglume_challenge_hash === challengeHash);
|
|
92
|
+
if (order) {
|
|
93
|
+
order.siglume_payment_status = "paid";
|
|
94
|
+
order.siglume_requirement_id = event.data.requirement_id || event.data.direct_payment_requirement_id;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
res.status(204).send();
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
app.use((error: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
|
102
|
+
const message = error instanceof Error ? error.message : "internal_error";
|
|
103
|
+
res.status(500).json({ error: message });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
app.listen(port);
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { DirectRequestPaymentMerchantClient } from "@siglume/direct-request-payment";
|
|
2
|
-
|
|
3
|
-
const merchant = new DirectRequestPaymentMerchantClient({
|
|
4
|
-
auth_token: process.env.SIGLUME_MERCHANT_AUTH_TOKEN,
|
|
5
|
-
});
|
|
6
|
-
|
|
7
|
-
const setup = await merchant.setupCheckout({
|
|
8
|
-
merchant: process.env.SIGLUME_DIRECT_PAYMENT_MERCHANT || "example_merchant",
|
|
9
|
-
display_name: process.env.SIGLUME_DIRECT_PAYMENT_DISPLAY_NAME || "Example Merchant",
|
|
10
|
-
billing_plan: process.env.SIGLUME_DIRECT_PAYMENT_PLAN || "launch",
|
|
11
|
-
billing_currency: process.env.SIGLUME_DIRECT_PAYMENT_BILLING_CURRENCY || "JPY",
|
|
12
|
-
webhook_callback_url: process.env.SIGLUME_DIRECT_PAYMENT_WEBHOOK_URL,
|
|
13
|
-
max_amount_minor: Number(process.env.SIGLUME_DIRECT_PAYMENT_BILLING_CAP_MINOR || 100000),
|
|
14
|
-
create_webhook_subscription: Boolean(process.env.SIGLUME_DIRECT_PAYMENT_WEBHOOK_URL),
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
console.log(JSON.stringify(setup, null, 2));
|
|
1
|
+
import { DirectRequestPaymentMerchantClient } from "@siglume/direct-request-payment";
|
|
2
|
+
|
|
3
|
+
const merchant = new DirectRequestPaymentMerchantClient({
|
|
4
|
+
auth_token: process.env.SIGLUME_MERCHANT_AUTH_TOKEN,
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
const setup = await merchant.setupCheckout({
|
|
8
|
+
merchant: process.env.SIGLUME_DIRECT_PAYMENT_MERCHANT || "example_merchant",
|
|
9
|
+
display_name: process.env.SIGLUME_DIRECT_PAYMENT_DISPLAY_NAME || "Example Merchant",
|
|
10
|
+
billing_plan: process.env.SIGLUME_DIRECT_PAYMENT_PLAN || "launch",
|
|
11
|
+
billing_currency: process.env.SIGLUME_DIRECT_PAYMENT_BILLING_CURRENCY || "JPY",
|
|
12
|
+
webhook_callback_url: process.env.SIGLUME_DIRECT_PAYMENT_WEBHOOK_URL,
|
|
13
|
+
max_amount_minor: Number(process.env.SIGLUME_DIRECT_PAYMENT_BILLING_CAP_MINOR || 100000),
|
|
14
|
+
create_webhook_subscription: Boolean(process.env.SIGLUME_DIRECT_PAYMENT_WEBHOOK_URL),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
console.log(JSON.stringify(setup, null, 2));
|