@siglume/direct-request-payment 0.1.0 → 0.3.0
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 +50 -0
- package/README.md +271 -104
- package/dist/index.cjs +253 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +131 -1
- package/dist/index.d.ts +131 -1
- package/dist/index.js +253 -1
- package/dist/index.js.map +1 -1
- package/docs/announcement-ja.md +64 -0
- package/docs/api-reference.md +209 -51
- package/docs/merchant-quickstart.md +183 -142
- package/docs/pricing.md +36 -19
- package/docs/security.md +14 -0
- package/examples/express-checkout.ts +3 -2
- package/examples/setup-merchant.ts +17 -0
- package/package.json +15 -1
|
@@ -14,39 +14,74 @@ external merchant.
|
|
|
14
14
|
The merchant server must not create charges with a customer wallet. It signs the
|
|
15
15
|
order challenge; the buyer-facing Siglume payment flow pays it.
|
|
16
16
|
|
|
17
|
-
## 1.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
17
|
+
## 1. Run Merchant Setup
|
|
18
|
+
|
|
19
|
+
Run setup from the merchant server, CI, or an integration agent with the
|
|
20
|
+
merchant's Siglume JWT. Do not use a Developer Portal `cli_` key here.
|
|
21
|
+
|
|
22
|
+
TypeScript:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { DirectRequestPaymentMerchantClient } from "@siglume/direct-request-payment";
|
|
26
|
+
|
|
27
|
+
const merchantClient = new DirectRequestPaymentMerchantClient({
|
|
28
|
+
auth_token: process.env.SIGLUME_MERCHANT_AUTH_TOKEN!,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const setup = await merchantClient.setupCheckout({
|
|
32
|
+
merchant: "example_merchant",
|
|
33
|
+
display_name: "Example Merchant",
|
|
34
|
+
billing_plan: "launch",
|
|
35
|
+
billing_currency: "JPY",
|
|
36
|
+
webhook_callback_url: "https://merchant.example/siglume/webhook",
|
|
37
|
+
max_amount_minor: 100000,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
console.log(setup.env);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Python:
|
|
44
|
+
|
|
45
|
+
```py
|
|
46
|
+
import os
|
|
47
|
+
|
|
48
|
+
from siglume_direct_request_payment import DirectRequestPaymentMerchantClient
|
|
49
|
+
|
|
50
|
+
merchant_client = DirectRequestPaymentMerchantClient(
|
|
51
|
+
auth_token=os.environ["SIGLUME_MERCHANT_AUTH_TOKEN"],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
setup = merchant_client.setup_checkout(
|
|
55
|
+
merchant="example_merchant",
|
|
56
|
+
display_name="Example Merchant",
|
|
57
|
+
billing_plan="launch",
|
|
58
|
+
billing_currency="JPY",
|
|
59
|
+
webhook_callback_url="https://merchant.example/siglume/webhook",
|
|
60
|
+
max_amount_minor=100000,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
print(setup["env"])
|
|
39
64
|
```
|
|
40
65
|
|
|
41
|
-
|
|
42
|
-
|
|
66
|
+
`setupCheckout` / `setup_checkout` performs:
|
|
67
|
+
|
|
68
|
+
- merchant key claim
|
|
69
|
+
- challenge secret creation
|
|
70
|
+
- billing mandate preparation
|
|
71
|
+
- webhook subscription creation for `direct_payment.confirmed` and
|
|
72
|
+
`direct_payment.spent`
|
|
73
|
+
|
|
74
|
+
Store `SIGLUME_DIRECT_PAYMENT_CHALLENGE_SECRET` and `SIGLUME_WEBHOOK_SECRET`
|
|
75
|
+
server-side only. Secrets are returned only when they are created or rotated.
|
|
76
|
+
If the returned billing mandate requires wallet approval, complete that Siglume
|
|
77
|
+
wallet step before accepting production payments.
|
|
43
78
|
|
|
44
79
|
## 2. Create an Order and Challenge
|
|
45
80
|
|
|
46
81
|
The merchant server creates the order before asking Siglume for payment.
|
|
47
82
|
|
|
48
|
-
```ts
|
|
49
|
-
import { createDirectRequestPaymentChallenge } from "@siglume/direct-request-payment";
|
|
83
|
+
```ts
|
|
84
|
+
import { createDirectRequestPaymentChallenge } from "@siglume/direct-request-payment";
|
|
50
85
|
|
|
51
86
|
const order = {
|
|
52
87
|
id: "order_123",
|
|
@@ -72,48 +107,48 @@ return {
|
|
|
72
107
|
amount_minor: order.amount_minor,
|
|
73
108
|
currency: order.currency,
|
|
74
109
|
siglume_challenge: challenge.challenge,
|
|
75
|
-
};
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
Python:
|
|
79
|
-
|
|
80
|
-
```py
|
|
81
|
-
import os
|
|
82
|
-
|
|
83
|
-
from siglume_direct_request_payment import create_direct_request_payment_challenge
|
|
84
|
-
|
|
85
|
-
order = {
|
|
86
|
-
"id": "order_123",
|
|
87
|
-
"amount_minor": 1200,
|
|
88
|
-
"currency": "JPY",
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
challenge = create_direct_request_payment_challenge(
|
|
92
|
-
merchant="example_merchant",
|
|
93
|
-
amount_minor=order["amount_minor"],
|
|
94
|
-
currency=order["currency"],
|
|
95
|
-
secret=os.environ["SIGLUME_DIRECT_PAYMENT_CHALLENGE_SECRET"],
|
|
96
|
-
nonce=f"{order['id']}-attempt_1",
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
orders.update(
|
|
100
|
-
order["id"],
|
|
101
|
-
{
|
|
102
|
-
"siglume_challenge_hash": challenge["challenge_hash"],
|
|
103
|
-
"siglume_payment_status": "pending",
|
|
104
|
-
},
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
"order_id": order["id"],
|
|
109
|
-
"amount_minor": order["amount_minor"],
|
|
110
|
-
"currency": order["currency"],
|
|
111
|
-
"siglume_challenge": challenge["challenge"],
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Never calculate `amount_minor` from browser input.
|
|
116
|
-
The nonce must be unique per order payment attempt and must not contain `:`.
|
|
110
|
+
};
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Python:
|
|
114
|
+
|
|
115
|
+
```py
|
|
116
|
+
import os
|
|
117
|
+
|
|
118
|
+
from siglume_direct_request_payment import create_direct_request_payment_challenge
|
|
119
|
+
|
|
120
|
+
order = {
|
|
121
|
+
"id": "order_123",
|
|
122
|
+
"amount_minor": 1200,
|
|
123
|
+
"currency": "JPY",
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
challenge = create_direct_request_payment_challenge(
|
|
127
|
+
merchant="example_merchant",
|
|
128
|
+
amount_minor=order["amount_minor"],
|
|
129
|
+
currency=order["currency"],
|
|
130
|
+
secret=os.environ["SIGLUME_DIRECT_PAYMENT_CHALLENGE_SECRET"],
|
|
131
|
+
nonce=f"{order['id']}-attempt_1",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
orders.update(
|
|
135
|
+
order["id"],
|
|
136
|
+
{
|
|
137
|
+
"siglume_challenge_hash": challenge["challenge_hash"],
|
|
138
|
+
"siglume_payment_status": "pending",
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
"order_id": order["id"],
|
|
144
|
+
"amount_minor": order["amount_minor"],
|
|
145
|
+
"currency": order["currency"],
|
|
146
|
+
"siglume_challenge": challenge["challenge"],
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Never calculate `amount_minor` from browser input.
|
|
151
|
+
The nonce must be unique per order payment attempt and must not contain `:`.
|
|
117
152
|
|
|
118
153
|
## 3. Buyer Creates and Pays the Requirement
|
|
119
154
|
|
|
@@ -121,8 +156,8 @@ After the buyer authenticates with Siglume, create the payment requirement with
|
|
|
121
156
|
the buyer's Siglume bearer token. Do not use a Developer Portal `cli_` API key
|
|
122
157
|
or merchant API key here.
|
|
123
158
|
|
|
124
|
-
```ts
|
|
125
|
-
import { DirectRequestPaymentClient } from "@siglume/direct-request-payment";
|
|
159
|
+
```ts
|
|
160
|
+
import { DirectRequestPaymentClient } from "@siglume/direct-request-payment";
|
|
126
161
|
|
|
127
162
|
const siglume = new DirectRequestPaymentClient({
|
|
128
163
|
auth_token: buyerSiglumeBearerToken,
|
|
@@ -133,26 +168,26 @@ const requirement = await siglume.createPaymentRequirement({
|
|
|
133
168
|
amount_minor: order.amount_minor,
|
|
134
169
|
currency: order.currency,
|
|
135
170
|
challenge: order.siglume_challenge,
|
|
136
|
-
});
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
Python:
|
|
140
|
-
|
|
141
|
-
```py
|
|
142
|
-
from siglume_direct_request_payment import DirectRequestPaymentClient
|
|
143
|
-
|
|
144
|
-
siglume = DirectRequestPaymentClient(auth_token=buyer_siglume_bearer_token)
|
|
145
|
-
|
|
146
|
-
requirement = siglume.create_payment_requirement(
|
|
147
|
-
merchant="example_merchant",
|
|
148
|
-
amount_minor=order["amount_minor"],
|
|
149
|
-
currency=order["currency"],
|
|
150
|
-
challenge=order["siglume_challenge"],
|
|
151
|
-
)
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
If Siglume returns `approve_transaction_request`, execute it first. Then execute
|
|
155
|
-
the payment transaction and verify the receipt.
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Python:
|
|
175
|
+
|
|
176
|
+
```py
|
|
177
|
+
from siglume_direct_request_payment import DirectRequestPaymentClient
|
|
178
|
+
|
|
179
|
+
siglume = DirectRequestPaymentClient(auth_token=buyer_siglume_bearer_token)
|
|
180
|
+
|
|
181
|
+
requirement = siglume.create_payment_requirement(
|
|
182
|
+
merchant="example_merchant",
|
|
183
|
+
amount_minor=order["amount_minor"],
|
|
184
|
+
currency=order["currency"],
|
|
185
|
+
challenge=order["siglume_challenge"],
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
If Siglume returns `approve_transaction_request`, execute it first. Then execute
|
|
190
|
+
the payment transaction and verify the receipt.
|
|
156
191
|
|
|
157
192
|
```ts
|
|
158
193
|
if (requirement.approve_transaction_request) {
|
|
@@ -163,31 +198,31 @@ const payment = await siglume.executePaymentTransaction(requirement, {
|
|
|
163
198
|
await_finality: true,
|
|
164
199
|
});
|
|
165
200
|
|
|
166
|
-
await siglume.verifyPaymentRequirement(requirement.requirement_id, {
|
|
167
|
-
receipt_id: String(payment.receipt?.receipt_id ?? ""),
|
|
168
|
-
});
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
Python:
|
|
172
|
-
|
|
173
|
-
```py
|
|
174
|
-
if requirement.get("approve_transaction_request"):
|
|
175
|
-
siglume.execute_allowance_transaction(requirement, await_finality=True)
|
|
176
|
-
|
|
177
|
-
payment = siglume.execute_payment_transaction(requirement, await_finality=True)
|
|
178
|
-
|
|
179
|
-
siglume.verify_payment_requirement(
|
|
180
|
-
requirement["requirement_id"],
|
|
181
|
-
receipt_id=str((payment.get("receipt") or {}).get("receipt_id") or ""),
|
|
182
|
-
)
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
## 4. Fulfill from Webhook
|
|
201
|
+
await siglume.verifyPaymentRequirement(requirement.requirement_id, {
|
|
202
|
+
receipt_id: String(payment.receipt?.receipt_id ?? ""),
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Python:
|
|
207
|
+
|
|
208
|
+
```py
|
|
209
|
+
if requirement.get("approve_transaction_request"):
|
|
210
|
+
siglume.execute_allowance_transaction(requirement, await_finality=True)
|
|
211
|
+
|
|
212
|
+
payment = siglume.execute_payment_transaction(requirement, await_finality=True)
|
|
213
|
+
|
|
214
|
+
siglume.verify_payment_requirement(
|
|
215
|
+
requirement["requirement_id"],
|
|
216
|
+
receipt_id=str((payment.get("receipt") or {}).get("receipt_id") or ""),
|
|
217
|
+
)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## 4. Fulfill from Webhook
|
|
186
221
|
|
|
187
222
|
Use the webhook as the durable signal, not just the browser return path.
|
|
188
223
|
|
|
189
|
-
```ts
|
|
190
|
-
import { verifyDirectRequestPaymentWebhook } from "@siglume/direct-request-payment";
|
|
224
|
+
```ts
|
|
225
|
+
import { verifyDirectRequestPaymentWebhook } from "@siglume/direct-request-payment";
|
|
191
226
|
|
|
192
227
|
const { event } = await verifyDirectRequestPaymentWebhook(
|
|
193
228
|
process.env.SIGLUME_WEBHOOK_SECRET!,
|
|
@@ -204,34 +239,34 @@ if (event.type === "direct_payment.confirmed") {
|
|
|
204
239
|
await orders.markPaidOnce(order.id, {
|
|
205
240
|
siglume_requirement_id: String(data.requirement_id ?? data.direct_payment_requirement_id ?? ""),
|
|
206
241
|
});
|
|
207
|
-
}
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
Python:
|
|
211
|
-
|
|
212
|
-
```py
|
|
213
|
-
import os
|
|
214
|
-
|
|
215
|
-
from siglume_direct_request_payment import verify_direct_request_payment_webhook
|
|
216
|
-
|
|
217
|
-
verified = verify_direct_request_payment_webhook(
|
|
218
|
-
os.environ["SIGLUME_WEBHOOK_SECRET"],
|
|
219
|
-
raw_request_body,
|
|
220
|
-
siglume_signature_header,
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
if verified["event"]["type"] == "direct_payment.confirmed":
|
|
224
|
-
data = verified["event"]["data"]
|
|
225
|
-
order = orders.find_by_challenge_hash(str(data.get("challenge_hash") or ""))
|
|
226
|
-
if not order:
|
|
227
|
-
raise RuntimeError("Unknown Siglume challenge hash")
|
|
228
|
-
orders.mark_paid_once(
|
|
229
|
-
order["id"],
|
|
230
|
-
siglume_requirement_id=str(data.get("requirement_id") or data.get("direct_payment_requirement_id") or ""),
|
|
231
|
-
)
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
## Failure Handling
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Python:
|
|
246
|
+
|
|
247
|
+
```py
|
|
248
|
+
import os
|
|
249
|
+
|
|
250
|
+
from siglume_direct_request_payment import verify_direct_request_payment_webhook
|
|
251
|
+
|
|
252
|
+
verified = verify_direct_request_payment_webhook(
|
|
253
|
+
os.environ["SIGLUME_WEBHOOK_SECRET"],
|
|
254
|
+
raw_request_body,
|
|
255
|
+
siglume_signature_header,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if verified["event"]["type"] == "direct_payment.confirmed":
|
|
259
|
+
data = verified["event"]["data"]
|
|
260
|
+
order = orders.find_by_challenge_hash(str(data.get("challenge_hash") or ""))
|
|
261
|
+
if not order:
|
|
262
|
+
raise RuntimeError("Unknown Siglume challenge hash")
|
|
263
|
+
orders.mark_paid_once(
|
|
264
|
+
order["id"],
|
|
265
|
+
siglume_requirement_id=str(data.get("requirement_id") or data.get("direct_payment_requirement_id") or ""),
|
|
266
|
+
)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Failure Handling
|
|
235
270
|
|
|
236
271
|
- `EXTERNAL_402_CHALLENGE_REQUIRED`: the merchant server did not provide a
|
|
237
272
|
challenge.
|
|
@@ -239,17 +274,23 @@ if verified["event"]["type"] == "direct_payment.confirmed":
|
|
|
239
274
|
signature does not match.
|
|
240
275
|
- `EXTERNAL_402_CHALLENGE_ALREADY_USED`: the challenge is already bound to a
|
|
241
276
|
different buyer.
|
|
242
|
-
- `
|
|
243
|
-
|
|
277
|
+
- `EXTERNAL_402_MERCHANT_NOT_FOUND`: run merchant setup with the merchant's
|
|
278
|
+
Siglume JWT.
|
|
279
|
+
- `EXTERNAL_402_MERCHANT_BILLING_SETUP_REQUIRED`: the merchant billing mandate
|
|
280
|
+
is not active yet.
|
|
244
281
|
- `EXTERNAL_402_MERCHANT_BILLING_PAST_DUE` or
|
|
245
282
|
`EXTERNAL_402_MERCHANT_BILLING_SUSPENDED`: merchant billing must be fixed
|
|
246
283
|
before new payments can be accepted.
|
|
247
284
|
|
|
248
285
|
## Go-Live Checklist
|
|
249
286
|
|
|
287
|
+
- `setupCheckout` / `setup_checkout` has claimed the merchant key.
|
|
288
|
+
- Merchant billing mandate is active.
|
|
250
289
|
- Challenge secret is only in server-side environment variables.
|
|
251
290
|
- Webhook endpoint receives raw body and verifies `Siglume-Signature`.
|
|
252
291
|
- Orders store `challenge_hash`, `requirement_id`, and fulfillment status.
|
|
253
292
|
- Fulfillment is idempotent.
|
|
254
293
|
- Browser input cannot change the amount or currency.
|
|
255
294
|
- Nonces cannot be reused for separate order attempts.
|
|
295
|
+
- The order is fulfilled only after a verified webhook maps back to the stored
|
|
296
|
+
`challenge_hash`.
|
package/docs/pricing.md
CHANGED
|
@@ -5,38 +5,55 @@ Payment as of 2026-06-11. Pricing can change by agreement or future product
|
|
|
5
5
|
release; the Siglume platform response is the source of truth for per-payment
|
|
6
6
|
fee data returned at runtime.
|
|
7
7
|
|
|
8
|
+
## Settlement Currencies
|
|
9
|
+
|
|
10
|
+
Siglume Direct Request Payment launches in the US and Japan, and both settlement
|
|
11
|
+
currencies are first-class:
|
|
12
|
+
|
|
13
|
+
- **JPY**, settled on-chain in **JPYC**
|
|
14
|
+
- **USD**, settled on-chain in **USDC**
|
|
15
|
+
|
|
16
|
+
A merchant settles in a single currency, chosen at onboarding. The settlement fee
|
|
17
|
+
percentage (the payment fee column below) is identical in both currencies. Only
|
|
18
|
+
the flat amounts — the monthly base fee and the per-payment minimum fee — are
|
|
19
|
+
quoted per currency.
|
|
20
|
+
|
|
8
21
|
## Trial Plans
|
|
9
22
|
|
|
10
|
-
| Plan | Monthly fee | Payment fee | Intended starting point |
|
|
11
|
-
| --- | ---: | ---: | --- |
|
|
12
|
-
| Launch | JPY 0 | 0
|
|
13
|
-
| Starter | JPY 980 | 1.0% | Early production checkout trials |
|
|
14
|
-
| Growth | JPY 2,980 | 0.7% | Growing EC, booking, membership, and API services |
|
|
15
|
-
| Pro | JPY 9,800 | 0.5% | Higher-volume merchant integrations |
|
|
23
|
+
| Plan | Monthly fee (JPY) | Monthly fee (USD) | Payment fee | Intended starting point |
|
|
24
|
+
| --- | ---: | ---: | ---: | --- |
|
|
25
|
+
| Launch | JPY 0 | USD 0 | 1.8% | Proofs of concept and low-volume trials |
|
|
26
|
+
| Starter | JPY 980 | USD 6.00 | 1.0% | Early production checkout trials |
|
|
27
|
+
| Growth | JPY 2,980 | USD 18.00 | 0.7% | Growing EC, booking, membership, and API services |
|
|
28
|
+
| Pro | JPY 9,800 | USD 60.00 | 0.5% | Higher-volume merchant integrations |
|
|
29
|
+
|
|
30
|
+
Every payment is fee-bearing at the plan rate. The minimum fee is JPY 30
|
|
31
|
+
(USD merchants: USD 0.20) per payment. The minimum covers the worst-case
|
|
32
|
+
per-payment settlement cost (an on-chain signature plus network gas), so small
|
|
33
|
+
payments are never processed at a loss; on larger payments the percentage rate
|
|
34
|
+
applies instead.
|
|
16
35
|
|
|
17
|
-
|
|
18
|
-
|
|
36
|
+
USD pricing is the JPY tier converted at roughly 160 JPY/USD and rounded to
|
|
37
|
+
clean price points that keep the same 1:3:10 tier ratio.
|
|
19
38
|
|
|
20
|
-
If no paid plan is selected during
|
|
39
|
+
If no paid plan is selected during merchant setup, the merchant account uses the
|
|
21
40
|
Launch plan. A merchant billing mandate is still required before accepting
|
|
22
|
-
payments so Siglume can collect
|
|
23
|
-
allowance is exceeded.
|
|
41
|
+
payments so Siglume can collect the monthly base fee automatically.
|
|
24
42
|
|
|
25
43
|
The current Siglume API and merchant registry may still expose the internal
|
|
26
44
|
`billing_plan` value `free` for the Launch tier. Treat `free` as an internal
|
|
27
|
-
compatibility key, not the public plan name.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
the allowance can continue when merchant billing is active, and those payments
|
|
31
|
-
are fee-bearing at the Launch overage rate.
|
|
45
|
+
compatibility key, not the public plan name. (Until 2026-06-12 the Launch plan
|
|
46
|
+
included a free monthly allowance of 100 payments; that allowance has been
|
|
47
|
+
retired — the platform `fee_bps` response is always the source of truth.)
|
|
32
48
|
|
|
33
49
|
Per-payment fees are collected during payment settlement through the
|
|
34
50
|
DirectPaymentHub split. The merchant receives the net amount after that fee.
|
|
35
51
|
Monthly base fees are collected separately through the merchant billing mandate.
|
|
36
52
|
|
|
37
|
-
The
|
|
38
|
-
|
|
39
|
-
|
|
53
|
+
The same fee schedule applies in JPY and USD. The Siglume platform returns
|
|
54
|
+
`fee_bps` in the merchant's settlement currency on every payment requirement, so
|
|
55
|
+
the SDK never has to know which currency table to read — it just trusts the
|
|
56
|
+
value Siglume returns.
|
|
40
57
|
|
|
41
58
|
## SDK Behavior
|
|
42
59
|
|
package/docs/security.md
CHANGED
|
@@ -14,6 +14,16 @@ These values must stay server-side:
|
|
|
14
14
|
The buyer-facing browser may receive the signed `challenge` string, but never
|
|
15
15
|
the secret that produced it.
|
|
16
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
|
+
|
|
17
27
|
## Bind the Order Server-Side
|
|
18
28
|
|
|
19
29
|
The HMAC challenge covers:
|
|
@@ -59,6 +69,10 @@ The signed payload is:
|
|
|
59
69
|
|
|
60
70
|
The default tolerance is 300 seconds.
|
|
61
71
|
|
|
72
|
+
Use verified webhook data as the durable completion signal. Browser redirects,
|
|
73
|
+
client-side callbacks, or local transaction responses can improve UX, but they
|
|
74
|
+
should not be the only source used to fulfill an order.
|
|
75
|
+
|
|
62
76
|
## Idempotency
|
|
63
77
|
|
|
64
78
|
Fulfill exactly once per order. Store at least:
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
|
|
8
8
|
const app = express();
|
|
9
9
|
const port = Number(process.env.PORT || 3000);
|
|
10
|
+
const merchantKey = process.env.SIGLUME_DIRECT_PAYMENT_MERCHANT || "example_merchant";
|
|
10
11
|
|
|
11
12
|
// Use JSON for normal routes. Use raw body only on the webhook route.
|
|
12
13
|
app.use((req, res, next) => {
|
|
@@ -35,7 +36,7 @@ app.post("/checkout/siglume/start", asyncRoute(async (req, res) => {
|
|
|
35
36
|
|
|
36
37
|
order.payment_attempt = Number(order.payment_attempt || 0) + 1;
|
|
37
38
|
const challenge = await createDirectRequestPaymentChallenge({
|
|
38
|
-
merchant:
|
|
39
|
+
merchant: merchantKey,
|
|
39
40
|
amount_minor: order.amount_minor,
|
|
40
41
|
currency: order.currency,
|
|
41
42
|
secret: process.env.SIGLUME_DIRECT_PAYMENT_CHALLENGE_SECRET!,
|
|
@@ -68,7 +69,7 @@ app.post("/checkout/siglume/pay", asyncRoute(async (req, res) => {
|
|
|
68
69
|
});
|
|
69
70
|
|
|
70
71
|
const requirement = await siglume.createPaymentRequirement({
|
|
71
|
-
merchant:
|
|
72
|
+
merchant: merchantKey,
|
|
72
73
|
amount_minor: order.amount_minor,
|
|
73
74
|
currency: order.currency,
|
|
74
75
|
challenge: String(req.body.siglume_challenge || ""),
|
|
@@ -0,0 +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));
|
package/package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siglume/direct-request-payment",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Merchant SDK for Siglume Direct Request Payment checkout integrations",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"siglume",
|
|
7
|
+
"payment",
|
|
8
|
+
"checkout",
|
|
9
|
+
"external-402",
|
|
10
|
+
"wallet",
|
|
11
|
+
"sdk"
|
|
12
|
+
],
|
|
5
13
|
"license": "MIT",
|
|
14
|
+
"author": "Siglume Contributors",
|
|
15
|
+
"homepage": "https://github.com/taihei-05/siglume-direct-request-payment#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/taihei-05/siglume-direct-request-payment/issues"
|
|
18
|
+
},
|
|
6
19
|
"repository": {
|
|
7
20
|
"type": "git",
|
|
8
21
|
"url": "git+https://github.com/taihei-05/siglume-direct-request-payment.git"
|
|
@@ -37,6 +50,7 @@
|
|
|
37
50
|
"docs",
|
|
38
51
|
"examples",
|
|
39
52
|
"README.md",
|
|
53
|
+
"CHANGELOG.md",
|
|
40
54
|
"LICENSE"
|
|
41
55
|
],
|
|
42
56
|
"scripts": {
|