@siglume/direct-request-payment 0.3.0 → 0.3.1

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.
@@ -1,296 +1,296 @@
1
- # Merchant Quickstart
2
-
3
- This guide shows the minimum safe Siglume Direct Request Payment flow for an
4
- external merchant.
5
-
6
- ## Actors
7
-
8
- - Merchant server: owns the order, amount, currency, challenge secret, webhook
9
- endpoint, and order fulfillment.
10
- - Buyer: owns the Siglume wallet that pays the DirectPaymentHub transaction.
11
- - Siglume: creates the payment requirement, prepares the wallet transaction,
12
- verifies the receipt, and emits signed webhooks.
13
-
14
- The merchant server must not create charges with a customer wallet. It signs the
15
- order challenge; the buyer-facing Siglume payment flow pays it.
16
-
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"])
64
- ```
65
-
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.
78
-
79
- ## 2. Create an Order and Challenge
80
-
81
- The merchant server creates the order before asking Siglume for payment.
82
-
83
- ```ts
84
- import { createDirectRequestPaymentChallenge } from "@siglume/direct-request-payment";
85
-
86
- const order = {
87
- id: "order_123",
88
- amount_minor: 1200,
89
- currency: "JPY",
90
- };
91
-
92
- const challenge = await createDirectRequestPaymentChallenge({
93
- merchant: "example_merchant",
94
- amount_minor: order.amount_minor,
95
- currency: order.currency,
96
- secret: process.env.SIGLUME_DIRECT_PAYMENT_CHALLENGE_SECRET!,
97
- nonce: `${order.id}-attempt_1`,
98
- });
99
-
100
- await orders.update(order.id, {
101
- siglume_challenge_hash: challenge.challenge_hash,
102
- siglume_payment_status: "pending",
103
- });
104
-
105
- return {
106
- order_id: order.id,
107
- amount_minor: order.amount_minor,
108
- currency: order.currency,
109
- siglume_challenge: challenge.challenge,
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 `:`.
152
-
153
- ## 3. Buyer Creates and Pays the Requirement
154
-
155
- After the buyer authenticates with Siglume, create the payment requirement with
156
- the buyer's Siglume bearer token. Do not use a Developer Portal `cli_` API key
157
- or merchant API key here.
158
-
159
- ```ts
160
- import { DirectRequestPaymentClient } from "@siglume/direct-request-payment";
161
-
162
- const siglume = new DirectRequestPaymentClient({
163
- auth_token: buyerSiglumeBearerToken,
164
- });
165
-
166
- const requirement = await siglume.createPaymentRequirement({
167
- merchant: "example_merchant",
168
- amount_minor: order.amount_minor,
169
- currency: order.currency,
170
- challenge: order.siglume_challenge,
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.
191
-
192
- ```ts
193
- if (requirement.approve_transaction_request) {
194
- await siglume.executeAllowanceTransaction(requirement, { await_finality: true });
195
- }
196
-
197
- const payment = await siglume.executePaymentTransaction(requirement, {
198
- await_finality: true,
199
- });
200
-
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
221
-
222
- Use the webhook as the durable signal, not just the browser return path.
223
-
224
- ```ts
225
- import { verifyDirectRequestPaymentWebhook } from "@siglume/direct-request-payment";
226
-
227
- const { event } = await verifyDirectRequestPaymentWebhook(
228
- process.env.SIGLUME_WEBHOOK_SECRET!,
229
- rawRequestBody,
230
- siglumeSignatureHeader,
231
- );
232
-
233
- if (event.type === "direct_payment.confirmed") {
234
- const data = event.data;
235
- const order = await orders.findByChallengeHash(String(data.challenge_hash ?? ""));
236
- if (!order) {
237
- throw new Error("Unknown Siglume challenge hash");
238
- }
239
- await orders.markPaidOnce(order.id, {
240
- siglume_requirement_id: String(data.requirement_id ?? data.direct_payment_requirement_id ?? ""),
241
- });
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
270
-
271
- - `EXTERNAL_402_CHALLENGE_REQUIRED`: the merchant server did not provide a
272
- challenge.
273
- - `INVALID_EXTERNAL_402_CHALLENGE`: the amount, currency, merchant, nonce, or
274
- signature does not match.
275
- - `EXTERNAL_402_CHALLENGE_ALREADY_USED`: the challenge is already bound to a
276
- different buyer.
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.
281
- - `EXTERNAL_402_MERCHANT_BILLING_PAST_DUE` or
282
- `EXTERNAL_402_MERCHANT_BILLING_SUSPENDED`: merchant billing must be fixed
283
- before new payments can be accepted.
284
-
285
- ## Go-Live Checklist
286
-
287
- - `setupCheckout` / `setup_checkout` has claimed the merchant key.
288
- - Merchant billing mandate is active.
289
- - Challenge secret is only in server-side environment variables.
290
- - Webhook endpoint receives raw body and verifies `Siglume-Signature`.
291
- - Orders store `challenge_hash`, `requirement_id`, and fulfillment status.
292
- - Fulfillment is idempotent.
293
- - Browser input cannot change the amount or currency.
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`.
1
+ # Merchant Quickstart
2
+
3
+ This guide shows the minimum safe Siglume Direct Request Payment flow for an
4
+ external merchant.
5
+
6
+ ## Actors
7
+
8
+ - Merchant server: owns the order, amount, currency, challenge secret, webhook
9
+ endpoint, and order fulfillment.
10
+ - Buyer: owns the Siglume wallet that pays the DirectPaymentHub transaction.
11
+ - Siglume: creates the payment requirement, prepares the wallet transaction,
12
+ verifies the receipt, and emits signed webhooks.
13
+
14
+ The merchant server must not create charges with a customer wallet. It signs the
15
+ order challenge; the buyer-facing Siglume payment flow pays it.
16
+
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"])
64
+ ```
65
+
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.
78
+
79
+ ## 2. Create an Order and Challenge
80
+
81
+ The merchant server creates the order before asking Siglume for payment.
82
+
83
+ ```ts
84
+ import { createDirectRequestPaymentChallenge } from "@siglume/direct-request-payment";
85
+
86
+ const order = {
87
+ id: "order_123",
88
+ amount_minor: 1200,
89
+ currency: "JPY",
90
+ };
91
+
92
+ const challenge = await createDirectRequestPaymentChallenge({
93
+ merchant: "example_merchant",
94
+ amount_minor: order.amount_minor,
95
+ currency: order.currency,
96
+ secret: process.env.SIGLUME_DIRECT_PAYMENT_CHALLENGE_SECRET!,
97
+ nonce: `${order.id}-attempt_1`,
98
+ });
99
+
100
+ await orders.update(order.id, {
101
+ siglume_challenge_hash: challenge.challenge_hash,
102
+ siglume_payment_status: "pending",
103
+ });
104
+
105
+ return {
106
+ order_id: order.id,
107
+ amount_minor: order.amount_minor,
108
+ currency: order.currency,
109
+ siglume_challenge: challenge.challenge,
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 `:`.
152
+
153
+ ## 3. Buyer Creates and Pays the Requirement
154
+
155
+ After the buyer authenticates with Siglume, create the payment requirement with
156
+ the buyer's Siglume bearer token. Do not use a Developer Portal `cli_` API key
157
+ or merchant API key here.
158
+
159
+ ```ts
160
+ import { DirectRequestPaymentClient } from "@siglume/direct-request-payment";
161
+
162
+ const siglume = new DirectRequestPaymentClient({
163
+ auth_token: buyerSiglumeBearerToken,
164
+ });
165
+
166
+ const requirement = await siglume.createPaymentRequirement({
167
+ merchant: "example_merchant",
168
+ amount_minor: order.amount_minor,
169
+ currency: order.currency,
170
+ challenge: order.siglume_challenge,
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.
191
+
192
+ ```ts
193
+ if (requirement.approve_transaction_request) {
194
+ await siglume.executeAllowanceTransaction(requirement, { await_finality: true });
195
+ }
196
+
197
+ const payment = await siglume.executePaymentTransaction(requirement, {
198
+ await_finality: true,
199
+ });
200
+
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
221
+
222
+ Use the webhook as the durable signal, not just the browser return path.
223
+
224
+ ```ts
225
+ import { verifyDirectRequestPaymentWebhook } from "@siglume/direct-request-payment";
226
+
227
+ const { event } = await verifyDirectRequestPaymentWebhook(
228
+ process.env.SIGLUME_WEBHOOK_SECRET!,
229
+ rawRequestBody,
230
+ siglumeSignatureHeader,
231
+ );
232
+
233
+ if (event.type === "direct_payment.confirmed") {
234
+ const data = event.data;
235
+ const order = await orders.findByChallengeHash(String(data.challenge_hash ?? ""));
236
+ if (!order) {
237
+ throw new Error("Unknown Siglume challenge hash");
238
+ }
239
+ await orders.markPaidOnce(order.id, {
240
+ siglume_requirement_id: String(data.requirement_id ?? data.direct_payment_requirement_id ?? ""),
241
+ });
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
270
+
271
+ - `EXTERNAL_402_CHALLENGE_REQUIRED`: the merchant server did not provide a
272
+ challenge.
273
+ - `INVALID_EXTERNAL_402_CHALLENGE`: the amount, currency, merchant, nonce, or
274
+ signature does not match.
275
+ - `EXTERNAL_402_CHALLENGE_ALREADY_USED`: the challenge is already bound to a
276
+ different buyer.
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.
281
+ - `EXTERNAL_402_MERCHANT_BILLING_PAST_DUE` or
282
+ `EXTERNAL_402_MERCHANT_BILLING_SUSPENDED`: merchant billing must be fixed
283
+ before new payments can be accepted.
284
+
285
+ ## Go-Live Checklist
286
+
287
+ - `setupCheckout` / `setup_checkout` has claimed the merchant key.
288
+ - Merchant billing mandate is active.
289
+ - Challenge secret is only in server-side environment variables.
290
+ - Webhook endpoint receives raw body and verifies `Siglume-Signature`.
291
+ - Orders store `challenge_hash`, `requirement_id`, and fulfillment status.
292
+ - Fulfillment is idempotent.
293
+ - Browser input cannot change the amount or currency.
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`.