@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.
@@ -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. Configure the Merchant
18
-
19
- During onboarding, Siglume assigns:
20
-
21
- - `merchant` key, for example `example_merchant`
22
- - challenge secret
23
- - allowed currency and token, initially `JPY`/`JPYC`; `USD`/`USDC` requires
24
- separately agreed merchant billing terms
25
- - billing plan; see [Pricing](./pricing.md)
26
-
27
- Keep the challenge secret server-side. Create a marketplace webhook subscription
28
- to receive the `whsec_` signing secret.
29
-
30
- ```bash
31
- curl -X POST https://siglume.com/v1/market/webhooks/subscriptions \
32
- -H "Authorization: Bearer <merchant-siglume-bearer-token>" \
33
- -H "Content-Type: application/json" \
34
- -d '{
35
- "callback_url": "https://merchant.example/siglume/webhook",
36
- "event_types": ["direct_payment.confirmed"],
37
- "description": "Direct Request Payment production webhook"
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
- The `signing_secret` is returned only when the subscription is created or
42
- rotated. Store it as `SIGLUME_WEBHOOK_SECRET`.
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
- - `EXTERNAL_402_MERCHANT_BILLING_SETUP_REQUIRED`: merchant onboarding is not
243
- complete.
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% through 100 payments/month, then 1.8% | Proofs of concept and low-volume trials |
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
- The minimum fee is JPY 3 for each fee-bearing payment, including Launch-plan
18
- payments after the included monthly allowance.
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 onboarding, the merchant account uses the
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 fees automatically after the 100-payment monthly
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
- The 100-payment monthly allowance is not a hard processing cap. Payments after
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 public trial pricing above is JPY-denominated. If a merchant needs USD/USDC
38
- settlement, agree the USD merchant billing terms during onboarding; do not infer
39
- USD monthly or minimum fees from the JPY table.
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: "example_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: "example_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.1.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": {