@siglume/direct-request-payment 0.4.19 → 0.4.22

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +18 -10
  3. package/bin/siglume-sdrp.mjs +550 -8
  4. package/dist/index.cjs +37 -3
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +27 -2
  7. package/dist/index.d.ts +27 -2
  8. package/dist/index.js +37 -3
  9. package/dist/index.js.map +1 -1
  10. package/docs/announcement-ja.md +17 -3
  11. package/docs/api-reference.md +60 -13
  12. package/docs/merchant-quickstart.md +6 -20
  13. package/docs/metered-statements.md +15 -13
  14. package/docs/payment-lifecycle.md +12 -9
  15. package/docs/pricing.md +7 -4
  16. package/docs/quickstart-10-minutes.md +134 -24
  17. package/docs/sandbox.md +60 -0
  18. package/docs/troubleshooting.md +23 -8
  19. package/examples/express-checkout.ts +37 -13
  20. package/examples/hosted-checkout-python/app.py +46 -31
  21. package/examples/hosted-checkout-python/order_store.py +13 -3
  22. package/examples/hosted-checkout-python/pyproject.toml +1 -1
  23. package/examples/hosted-checkout-typescript/src/order-store.ts +14 -3
  24. package/examples/hosted-checkout-typescript/src/server.ts +49 -37
  25. package/package.json +10 -2
  26. package/templates/express/README.md +40 -6
  27. package/templates/express/siglume-order-store.example.ts +22 -6
  28. package/templates/express/siglume-order-store.sql.ts +585 -0
  29. package/templates/express/siglume-sdrp-routes.ts +138 -64
  30. package/templates/fastapi/README.md +22 -3
  31. package/templates/fastapi/siglume_order_store_example.py +29 -6
  32. package/templates/fastapi/siglume_order_store_sqlalchemy.py +313 -0
  33. package/templates/fastapi/siglume_sdrp_routes.py +112 -49
@@ -48,6 +48,8 @@ Standard can be marked paid only after settled per-payment finality, while Micro
48
48
  | `SIGLUME_MERCHANT_AUTH_TOKEN` | merchant setup helper | merchant Siglume bearer token for self-service setup |
49
49
  | `SIGLUME_AUTH_TOKEN` | user-authenticated helper | buyer Siglume bearer token for payment / buyer statements, or provider Siglume bearer token for provider statements |
50
50
  | `SIGLUME_API_BASE` | optional | API base URL override; defaults to `https://siglume.com/v1` |
51
+ | `SIGLUME_ENV` | optional | Set to `sandbox` to default clients and readiness to the local sandbox API |
52
+ | `SIGLUME_SANDBOX_API_BASE` | optional | Sandbox API override; defaults to `http://127.0.0.1:8787/v1` when `SIGLUME_ENV=sandbox` |
51
53
  | `SIGLUME_WEBHOOK_SECRET` | merchant server | webhook signing secret returned as `whsec_...` |
52
54
 
53
55
  Do not use a Developer Portal `cli_` API key as either auth token. Merchant
@@ -356,7 +358,8 @@ Returns:
356
358
  `merchant.merchant_account.metadata_jsonb.metered_risk_acceptance` records the
357
359
  merchant's Micro / Nano delayed-settlement risk acceptance receipt with
358
360
  `terms_version`, `accepted_at`, `principal_user_id`, `receipt_id`, and fixed
359
- market thresholds `JPY: 10000` / `USD: 10000`.
361
+ market thresholds JPY 10,000 / USD 100.00 (`settlement_threshold_minor` is
362
+ `10000` for both JPY minor units and USD cents).
360
363
 
361
364
  Secrets are returned only when created or rotated. Existing secrets are not
362
365
  replayed by `getMerchant` / `get_merchant`.
@@ -372,7 +375,9 @@ POST /v1/sdrp/direct-payments/merchants
372
375
  Creates or updates the merchant account for the authenticated merchant user.
373
376
  Accepts the optional `checkout_allowed_origins: string[]` return-URL origin
374
377
  allowlist described under `setupCheckout` above; the same normalization and
375
- webhook-origin auto-allow apply.
378
+ webhook-origin auto-allow apply. Python annotates this direct response as
379
+ `DirectRequestPaymentMerchantResponse`; `setup_checkout(...)` returns
380
+ `DirectRequestPaymentCheckoutSetupResult`.
376
381
 
377
382
  ### `createCheckoutSession(input)` / `create_checkout_session(...)`
378
383
 
@@ -535,6 +540,41 @@ Defaults event types to `direct_payment.confirmed` and
535
540
  `direct_payment.spent`. The returned `signing_secret` is shown only at creation
536
541
  or rotation.
537
542
 
543
+ ### `listWebhookSubscriptions()` / `list_webhook_subscriptions()`
544
+
545
+ Calls:
546
+
547
+ ```text
548
+ GET /v1/market/webhooks/subscriptions
549
+ ```
550
+
551
+ Returns the current user's webhook subscriptions without the full signing
552
+ secret. Use `signing_secret_hint` to confirm that the local
553
+ `SIGLUME_WEBHOOK_SECRET` is the expected secret.
554
+
555
+ ### `queueWebhookTestDelivery(input)` / `queue_webhook_test_delivery(...)`
556
+
557
+ Calls:
558
+
559
+ ```text
560
+ POST /v1/market/webhooks/test-deliveries
561
+ ```
562
+
563
+ Queues a signed test event to one or more subscription ids. `siglume-check
564
+ readiness` uses this for a harmless `direct_payment.confirmed` readiness probe.
565
+
566
+ ### `listWebhookDeliveries(input)` / `list_webhook_deliveries(...)`
567
+
568
+ Calls:
569
+
570
+ ```text
571
+ GET /v1/market/webhooks/deliveries
572
+ ```
573
+
574
+ Supports `subscription_id`, `event_type`, `status`, and `limit`. Readiness polls
575
+ this list after queueing the test delivery and only passes when the matching
576
+ delivery status becomes `delivered`.
577
+
538
578
  ## `DirectRequestPaymentClient`
539
579
 
540
580
  Thin wrapper around the current Siglume Direct Request Payment HTTP contract.
@@ -1012,10 +1052,10 @@ the `Siglume-Signature` header; use
1012
1052
  `verifyDirectRequestPaymentWebhook(...)` /
1013
1053
  `verify_direct_request_payment_webhook(...)` for signature verification and
1014
1054
  parsing together. Throws
1015
- `SiglumeWebhookPayloadError` on a malformed event, or when a
1016
- `direct_payment.confirmed` event does not carry a supported Direct Request
1017
- Payment mode (`external_402` or `metered_settlement_batch`). The `payload`
1018
- argument is positional in both languages.
1055
+ `SiglumeWebhookPayloadError` on a malformed event. It does not reject an
1056
+ unsupported `direct_payment.confirmed` mode by itself; the classifier returns
1057
+ `kind: "unknown"` with `reason: "unsupported_confirmation_mode"` for that case.
1058
+ The `payload` argument is positional in both languages.
1019
1059
 
1020
1060
  For `direct_payment.confirmed`, inspect `event.data.pricing_band`,
1021
1061
  `event.data.settlement_cadence`, `event.data.finality`,
@@ -1039,13 +1079,15 @@ Recommended branch: call `classifyDirectPaymentConfirmation(event)` /
1039
1079
  pricing, per-payment on-chain finality, settled status, non-empty
1040
1080
  `requirement_id`, non-empty `challenge_hash`, and non-empty
1041
1081
  `chain_receipt_id`.
1042
- - `metered_usage_accepted`: treat the usage as accepted but unsettled. This
1043
- requires Micro / Nano pricing, `finality === "aggregated_onchain_settlement"`,
1044
- the matching settlement cadence (`micro` -> `weekly`, `nano` -> `monthly`),
1082
+ - `metered_usage_accepted`: treat the usage as accepted but unsettled only if
1083
+ your integration has explicitly enabled Micro / Nano delayed-settlement
1084
+ handling. This requires Micro / Nano pricing,
1085
+ `finality === "aggregated_onchain_settlement"`, the matching settlement
1086
+ cadence (`micro` -> `weekly`, `nano` -> `monthly`),
1045
1087
  `settlement_status === "pending_settlement"`, non-empty `requirement_id`, and
1046
- non-empty `challenge_hash`. SDRP merchant setup and terms assume the merchant
1047
- accepts this delayed aggregated settlement model for Micro / Nano amount
1048
- bands; reconcile final revenue from statement APIs / settlement batches.
1088
+ non-empty `challenge_hash`. Standard-only integrations should route this to
1089
+ review or return `METERED_INTEGRATION_REQUIRED`; Micro / Nano integrations
1090
+ must reconcile final revenue from statement APIs / settlement batches.
1049
1091
  - `unknown`: do not mark paid or fulfilled from the event type alone; fetch the
1050
1092
  requirement or route the event to manual review.
1051
1093
 
@@ -1153,7 +1195,11 @@ package exports `TypedDict` names for the high-risk response shapes:
1153
1195
  - `DirectRequestPaymentPastDueBlock`
1154
1196
  - `DirectRequestPaymentProviderMeteredTotals`
1155
1197
  - `DirectRequestPaymentListResponse`
1156
- - `DirectRequestPaymentMerchantSetupResponse`
1198
+ - `DirectRequestPaymentMerchantResponse`
1199
+ - `DirectRequestPaymentCheckoutSetupResult`
1200
+ - `DirectRequestPaymentMerchantSetupResponse` (compatibility alias for checkout setup result)
1201
+ - `DirectRequestPaymentWebhookSubscription`
1202
+ - `DirectRequestPaymentWebhookDelivery`
1157
1203
  - `DirectRequestPaymentWebhookVerification`
1158
1204
  - `DirectRequestPaymentConfirmationClassification`
1159
1205
 
@@ -1170,6 +1216,7 @@ Both packages export these importable constants:
1170
1216
  | Constant | Value |
1171
1217
  | --- | --- |
1172
1218
  | `DEFAULT_SIGLUME_API_BASE` | `https://siglume.com/v1` |
1219
+ | `DEFAULT_SIGLUME_SANDBOX_API_BASE` | `http://127.0.0.1:8787/v1` |
1173
1220
  | `DIRECT_REQUEST_PAYMENT_CHALLENGE_SCHEME` | `siglume-external-402-v1` |
1174
1221
  | `DIRECT_REQUEST_PAYMENT_RECURRING_CHALLENGE_SCHEME` | `siglume-external-402-recurring-v1` |
1175
1222
  | `DIRECT_REQUEST_PAYMENT_MODE` | `external_402` |
@@ -459,16 +459,9 @@ if (confirmation.kind === "standard_settled") {
459
459
  }
460
460
 
461
461
  if (confirmation.kind === "metered_usage_accepted") {
462
- const order = await orders.findByChallengeHash(confirmation.challenge_hash);
463
- if (!order) {
464
- await orders.flagForPaymentStateReview({
465
- reason: "unknown_metered_challenge_hash",
466
- requirement_id: confirmation.requirement_id,
467
- });
468
- return new Response(null, { status: 204 });
469
- }
470
- await orders.markFulfilledButUnsettledOnce(order.id, {
471
- siglume_requirement_id: confirmation.requirement_id,
462
+ await orders.flagForPaymentStateReview({
463
+ reason: "metered_integration_required",
464
+ requirement_id: confirmation.requirement_id,
472
465
  pricing_band: confirmation.pricing_band,
473
466
  });
474
467
  return new Response(null, { status: 204 });
@@ -531,16 +524,9 @@ if confirmation["kind"] == "standard_settled":
531
524
  return "", 204
532
525
 
533
526
  if confirmation["kind"] == "metered_usage_accepted":
534
- order = orders.find_by_challenge_hash(confirmation["challenge_hash"])
535
- if not order:
536
- orders.flag_for_payment_state_review(
537
- reason="unknown_metered_challenge_hash",
538
- requirement_id=confirmation["requirement_id"],
539
- )
540
- return "", 204
541
- orders.mark_fulfilled_but_unsettled_once(
542
- order["id"],
543
- siglume_requirement_id=confirmation["requirement_id"],
527
+ orders.flag_for_payment_state_review(
528
+ reason="metered_integration_required",
529
+ requirement_id=confirmation["requirement_id"],
544
530
  pricing_band=confirmation["pricing_band"],
545
531
  )
546
532
  return "", 204
@@ -195,9 +195,12 @@ Threshold-control fields:
195
195
  - `settlement_trigger`: `amount_threshold` or `scheduled_close`
196
196
  - `settlement_threshold_minor`: JPY `10000` or USD `10000` minor units
197
197
  - `threshold_reached_at`: set when the fixed amount threshold closed the batch
198
- - `total_unsettled_exposure_minor`: open plus `notice_pending`, `ready`,
199
- `submitted`, retrying, and `past_due` provider gross exposure for the same
200
- buyer / provider / token / pricing band
198
+ - `total_unsettled_exposure_minor`: chargeable provider gross exposure for the
199
+ same buyer / provider / token / pricing band where the batch is not
200
+ `settled`, `uncollectible`, or `written_off`. This includes open usage,
201
+ `notice_pending`, `notice_delivery_failed`, `ready`, `submitted`,
202
+ `submitted_reconcile_required`, `failed_retryable`, `retrying`, and
203
+ `past_due`.
201
204
 
202
205
  JPY 10,000 and USD 100.00 are market-specific fixed thresholds, not FX
203
206
  conversions of one another.
@@ -318,7 +321,7 @@ Important batch fields:
318
321
  | `settlement_trigger` | `amount_threshold` for early threshold close, or `scheduled_close` for weekly/monthly close |
319
322
  | `settlement_threshold_minor` | Fixed market threshold for early settlement: JPY `10000` or USD `10000` minor units |
320
323
  | `threshold_reached_at` | Timestamp when the fixed threshold closed the batch, otherwise null |
321
- | `total_unsettled_exposure_minor` | Current open plus notice/ready/submitted/retrying/past-due provider gross exposure for the same buyer / provider / token / pricing band |
324
+ | `total_unsettled_exposure_minor` | Chargeable provider gross exposure for the same buyer / provider / token / pricing band where status is not `settled`, `uncollectible`, or `written_off`; includes open, notice, ready, submitted, reconcile-required, retryable, retrying, and past-due states |
322
325
  | `expected_scheduled_debit_at` | Expected debit time for an open period before a batch exists |
323
326
  | `scheduled_debit_at` | Scheduled debit time after batch creation |
324
327
  | `not_before_attempt_at` | Earliest allowed debit attempt; this is the close-plus-3-day gate |
@@ -398,10 +401,10 @@ Siglume retries failed Micro / Nano settlement every 6 hours for up to 28
398
401
  automatic attempts. After that the batch remains `past_due` until operator
399
402
  requeue.
400
403
 
401
- New Micro / Nano usage for the same buyer / provider / token is paused while
402
- the total unsettled exposure is at or above the fixed threshold, and while a
403
- failed or past-due block remains. The provider API is not called for the
404
- rejected request, and the request is not charged.
404
+ New Micro / Nano usage for the same buyer / provider / token / pricing band is
405
+ paused while the total unsettled exposure is at or above the fixed threshold,
406
+ and while a failed or past-due block remains. The provider API is not called for
407
+ the rejected request, and the request is not charged.
405
408
 
406
409
  Public failure fields are sanitized. Show `failure_reason_code`,
407
410
  `failure_reason_label`, `failure_reason_help`, and `support_reference` to users
@@ -449,13 +452,12 @@ using stable idempotency keys and provider-side completion records.
449
452
  | --- | --- | --- | --- | --- |
450
453
  | `notice_delivery_failed` | Buyer debit is not yet allowed; provider revenue remains unsettled | Notice delivery can be retried or reviewed | Required if delivery keeps failing | Do not attempt your own debit notice or mark revenue settled. Show support context only. |
451
454
  | `submitted_reconcile_required` | A settlement submission exists but final on-chain outcome is not yet reconciled | Reconciliation may complete if a receipt is found | Required if reconciliation stalls | Do not retry payment yourself. Wait for `settled`, `failed_retryable`, or `past_due`. |
452
- | `past_due` | Buyer has an unresolved settlement block; provider sees past-due revenue | New Micro / Nano usage for the same buyer / plan / token is paused | Operator requeue or manual resolution only | Do not promise collection or provider payment. Ask the buyer to repair balance / allowance / BudgetVault / caps and reference `support_reference`. |
455
+ | `past_due` | Buyer has an unresolved settlement block; provider sees past-due revenue | New Micro / Nano usage for the same buyer / provider / token / pricing band is paused | Operator requeue or manual resolution only | Do not promise collection or provider payment. Ask the buyer to repair balance / allowance / BudgetVault / caps and reference `support_reference`. |
453
456
  | `failed_chargeable` | Usage is still chargeable because provider work was accepted or completed | Included in later settlement attempts | Review if the provider disputes completion | Keep fulfillment idempotent and preserve evidence keyed by idempotency key. |
454
457
 
455
- Future platform versions may add explicit terminal states such as
456
- `closed_unpaid`, `uncollectible`, or `written_off`. Treat unknown terminal
457
- settlement states as not settled unless `status === "settled"` and
458
- `chain_receipt_id` is present.
458
+ Terminal public states include `uncollectible` and `written_off` after operator
459
+ review. Treat unknown terminal settlement states as not settled unless
460
+ `status === "settled"` and `chain_receipt_id` is present.
459
461
 
460
462
  ## Operational Recipes
461
463
 
@@ -32,7 +32,7 @@ checkout open or agent/API payment starts
32
32
  -> usage accepted
33
33
  -> direct_payment.confirmed webhook
34
34
  -> classifier kind: metered_usage_accepted
35
- -> merchant may fulfill as fulfilled_unsettled
35
+ -> merchant may fulfill as fulfilled_unsettled only after enabling Micro/Nano handling
36
36
  -> open period closes by amount threshold or schedule
37
37
  -> final notice window
38
38
  -> submitted / retrying / past_due if needed
@@ -41,10 +41,12 @@ checkout open or agent/API payment starts
41
41
  -> provider revenue is settled
42
42
  ```
43
43
 
44
- For Micro / Nano, `metered_usage_accepted` means the usage can be fulfilled
45
- under the SDRP delayed settlement model, but provider revenue is not settled
46
- yet. Provider revenue becomes settled only when the settlement batch is settled
47
- on-chain and has a `chain_receipt_id`.
44
+ For Micro / Nano, `metered_usage_accepted` means the usage can be fulfilled only
45
+ by an integration that has explicitly accepted SDRP delayed settlement and
46
+ implemented fulfilled-but-unsettled state, settlement reconciliation, past-due
47
+ handling, and terminal accounting. Provider revenue is not settled yet. Provider
48
+ revenue becomes settled only when the settlement batch is settled on-chain and
49
+ has a `chain_receipt_id`.
48
50
 
49
51
  ## Field meanings
50
52
 
@@ -52,7 +54,7 @@ on-chain and has a `chain_receipt_id`.
52
54
  | --- | --- | --- |
53
55
  | Hosted Checkout `status: "paid"` | The checkout session accepted the wallet payment flow. | For Micro / Nano, it does not mean provider revenue is settled. |
54
56
  | `standard_settled` | Standard payment is on-chain settled and can mark an order paid. | It is not used for Micro / Nano accepted usage. |
55
- | `metered_usage_accepted` | Micro / Nano usage is accepted and may be fulfilled as unsettled. | It is not settled provider revenue. |
57
+ | `metered_usage_accepted` | Micro / Nano usage is accepted for integrations that have enabled delayed settlement handling. | It is not settled provider revenue, and Standard-only integrations should not fulfill it. |
56
58
  | `fulfilled_unsettled` | Your merchant system delivered the item before Micro / Nano settlement. | It is not a Siglume settlement status. |
57
59
  | `metered_batch_settled` | Aggregated Micro / Nano batch settled on-chain. | It does not identify one order by challenge hash. |
58
60
  | `pending_settlement` | Micro / Nano usage is waiting for aggregated settlement. | It is not a failure by itself. |
@@ -65,9 +67,10 @@ on-chain and has a `chain_receipt_id`.
65
67
  re-stringified JSON object.
66
68
  - Store `challenge_hash` on the order before redirecting the buyer.
67
69
  - For Standard, mark paid only from `standard_settled`.
68
- - For Micro / Nano, use a separate local state such as
69
- `fulfilled_unsettled`; reconcile final revenue from statement APIs and batch
70
- settlement events.
70
+ - For Micro / Nano, fulfill only after you have explicitly enabled delayed
71
+ settlement handling. Use a separate local state such as
72
+ `fulfilled_unsettled`, then reconcile final revenue from statement APIs and
73
+ batch settlement events.
71
74
  - Treat `unknown` classifications as manual review. Do not mark paid or
72
75
  fulfilled from the event name alone.
73
76
 
package/docs/pricing.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Pricing
2
2
 
3
3
  This page documents the trial-phase merchant pricing for Siglume Direct Request
4
- Payment as of SDK v0.4.19. Pricing can change by agreement or future product
4
+ Payment as of SDK v0.4.22. 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
 
@@ -156,9 +156,12 @@ confirmed payment turns into money in your settlement wallet.
156
156
  - While the same buyer / provider / token / pricing band has total unsettled
157
157
  exposure at or above the fixed threshold, new Micro/Nano usage is paused with the
158
158
  machine-readable error `METERED_SETTLEMENT_PAST_DUE`; the provider API is not
159
- called. Exposure includes open usage plus `notice_pending`, `ready`,
160
- `submitted`, retrying, and `past_due` batches, and remains paused while
161
- settlement failure or `past_due` is unresolved.
159
+ called. Exposure is chargeable provider gross where status is not `settled`,
160
+ `uncollectible`, or `written_off`; it includes open usage,
161
+ `notice_pending`, `notice_delivery_failed`, `ready`, `submitted`,
162
+ `submitted_reconcile_required`, `failed_retryable`, `retrying`, and
163
+ `past_due`. Usage remains paused while settlement failure or `past_due` is
164
+ unresolved.
162
165
  - Outstanding amounts remain attached to the failed settlement and are retried
163
166
  under this policy. They are not settled revenue, and Siglume does not advance,
164
167
  guarantee, or insure provider revenue before on-chain settlement succeeds.
@@ -11,7 +11,43 @@ The SDK supplies the readiness check, route files, webhook verification, payment
11
11
  classification, and the order-store adapter contract. Your app supplies the
12
12
  real order lookup and fulfillment writes.
13
13
 
14
- ## 0. Readiness first
14
+ ## 0. Run the sandbox first
15
+
16
+ Do this before touching live Siglume credentials. The local sandbox is a tiny
17
+ Siglume-compatible API server bundled with the SDK. It creates fake checkout
18
+ sessions, signs webhooks with your sandbox secret, records delivery status, and
19
+ never charges a wallet.
20
+
21
+ In one terminal, point it at your product's local webhook route:
22
+
23
+ ```bash
24
+ npx siglume-sdrp sandbox \
25
+ --origin http://localhost:3000 \
26
+ --webhook-url http://localhost:3000/payments/webhooks/siglume
27
+ ```
28
+
29
+ Use the values it prints in your product `.env`:
30
+
31
+ ```bash
32
+ SIGLUME_ENV=sandbox
33
+ SIGLUME_API_BASE=http://127.0.0.1:8787/v1
34
+ SIGLUME_MERCHANT_AUTH_TOKEN=sandbox_merchant_token
35
+ SIGLUME_DIRECT_PAYMENT_MERCHANT=sandbox_merchant
36
+ SHOP_PUBLIC_ORIGIN=http://localhost:3000
37
+ SHOP_WEBHOOK_URL=http://localhost:3000/payments/webhooks/siglume
38
+ SIGLUME_WEBHOOK_SECRET=whsec_sandbox_local
39
+ ```
40
+
41
+ Then run:
42
+
43
+ ```bash
44
+ npx siglume-check readiness --sandbox
45
+ ```
46
+
47
+ The sandbox readiness check proves your local server can receive a signed
48
+ `direct_payment.confirmed` delivery before you use live credentials.
49
+
50
+ ## 1. Live readiness
15
51
 
16
52
  Install the SDK in your product.
17
53
 
@@ -34,29 +70,37 @@ SIGLUME_MERCHANT_AUTH_TOKEN=<merchant Siglume bearer token>
34
70
  SIGLUME_DIRECT_PAYMENT_MERCHANT=<merchant key>
35
71
  SHOP_PUBLIC_ORIGIN=https://www.your-product.example
36
72
  SHOP_WEBHOOK_URL=https://api.your-product.example/payments/webhooks/siglume
73
+ SIGLUME_WEBHOOK_SECRET=<webhook signing secret from setupCheckout/setup_checkout>
37
74
  ```
38
75
 
39
- Then run:
76
+ Then run the matching CLI:
40
77
 
41
78
  ```bash
79
+ # Node / Express
42
80
  npx siglume-check readiness
81
+
82
+ # Python / FastAPI
83
+ siglume-check readiness
43
84
  ```
44
85
 
45
86
  The readiness check fails before you write checkout code if any required item is
46
- missing. It checks local config, reads the merchant account, and creates one
47
- unpaid expiring Hosted Checkout probe session to prove the account is enabled
48
- and the return origin is accepted. No buyer is charged.
87
+ missing. It checks local config, reads the merchant account, requires active
88
+ billing, confirms the webhook subscription points to `SHOP_WEBHOOK_URL`, checks
89
+ that `direct_payment.confirmed` is subscribed, verifies the local webhook secret
90
+ against the subscription hint, creates one unpaid expiring Hosted Checkout probe
91
+ session, and queues a signed webhook test delivery. No buyer is charged.
49
92
 
50
- For CI or a preflight script:
93
+ For a CI local-config smoke test:
51
94
 
52
95
  ```bash
53
- npx siglume-check readiness --json
96
+ npx siglume-check readiness --no-api --json
54
97
  ```
55
98
 
56
- If this command fails, fix the reported item first. Do not build a human web
57
- checkout path until readiness passes.
99
+ `--no-api` does not prove Hosted Checkout or webhook delivery. Before opening a
100
+ human web checkout path, run readiness without `--no-api` and fix every FAIL
101
+ item.
58
102
 
59
- ## 1. Copy integration files into your product
103
+ ## 2. Copy integration files into your product
60
104
 
61
105
  For Express:
62
106
 
@@ -73,21 +117,36 @@ siglume-sdrp init fastapi --target app/siglume
73
117
  These commands copy framework-specific route files into your codebase. The
74
118
  generated files are intentionally small and are meant to be edited.
75
119
 
76
- ## 2. Mount the routes
120
+ ## 3. Mount the routes
77
121
 
78
122
  Express:
79
123
 
80
124
  ```ts
81
- import { createSiglumeSdrpRouter } from "./siglume/siglume-sdrp-routes.js";
125
+ import express from "express";
126
+ import {
127
+ createSiglumeSdrpCheckoutRouter,
128
+ createSiglumeSdrpWebhookHandler,
129
+ type SiglumeSdrpRouterOptions,
130
+ } from "./siglume/siglume-sdrp-routes.js";
82
131
  import { siglumeOrderStore } from "./siglume/siglume-order-store.example.js";
83
132
 
84
- app.use("/payments", createSiglumeSdrpRouter({
133
+ const siglumeOptions: SiglumeSdrpRouterOptions = {
85
134
  merchant: process.env.SIGLUME_DIRECT_PAYMENT_MERCHANT!,
86
135
  merchant_auth_token: process.env.SIGLUME_MERCHANT_AUTH_TOKEN!,
87
136
  webhook_secret: process.env.SIGLUME_WEBHOOK_SECRET!,
88
137
  shop_public_origin: process.env.SHOP_PUBLIC_ORIGIN!,
89
138
  order_store: siglumeOrderStore,
90
- }));
139
+ allow_metered_payments: false,
140
+ };
141
+
142
+ app.post(
143
+ "/payments/webhooks/siglume",
144
+ express.raw({ type: "application/json" }),
145
+ createSiglumeSdrpWebhookHandler(siglumeOptions),
146
+ );
147
+
148
+ app.use(express.json());
149
+ app.use("/payments", createSiglumeSdrpCheckoutRouter(siglumeOptions));
91
150
  ```
92
151
 
93
152
  FastAPI:
@@ -97,27 +156,77 @@ from .siglume.siglume_order_store_example import ExampleSiglumeOrderStore
97
156
  from .siglume.siglume_sdrp_routes import create_siglume_sdrp_router
98
157
 
99
158
  app.include_router(
100
- create_siglume_sdrp_router(ExampleSiglumeOrderStore()),
159
+ create_siglume_sdrp_router(ExampleSiglumeOrderStore(), allow_metered_payments=False),
101
160
  prefix="/payments",
102
161
  )
103
162
  ```
104
163
 
105
- ## 3. Replace the order-store example
164
+ ## 4. Adapter responsibilities
106
165
 
107
166
  Replace the example store with your product's order database. The adapter must:
108
167
 
109
168
  - load the order by your `order_id`,
110
169
  - verify the current user is allowed to pay for that order,
111
170
  - return the server-authored `amount_minor` and `currency`,
112
- - persist `challenge_hash` and `checkout_session_id` before redirecting,
113
- - record webhook event ids durably,
171
+ - create or reuse one active checkout attempt with a stable nonce,
172
+ - persist `challenge_hash`, `checkout_session_id`, and `checkout_url` before redirecting,
173
+ - process webhook event ids durably in the same transaction as the order update,
114
174
  - mark Standard orders paid exactly once,
115
- - mark Micro / Nano orders as fulfilled but unsettled exactly once,
116
175
  - route unknown classifications to manual review.
117
176
 
118
177
  Do not calculate the amount from browser input.
119
178
 
120
- ## 4. Start checkout from your frontend
179
+ The generated route defaults to Standard-only. If an order amount falls into
180
+ Micro / Nano, checkout returns `METERED_INTEGRATION_REQUIRED` until you set
181
+ `allow_metered_payments: true` / `allow_metered_payments=True` and implement
182
+ fulfilled-but-unsettled state, settlement reconciliation, past-due handling, and
183
+ terminal write-off handling.
184
+
185
+ ## 5. Use a real database adapter
186
+
187
+ The copied files include durable database adapters. Use these before opening
188
+ checkout to users; the `*.example.*` stores are only for reading the interface.
189
+
190
+ Express:
191
+
192
+ ```ts
193
+ import {
194
+ createPrismaSiglumeOrderStore,
195
+ createTypeOrmSiglumeOrderStore,
196
+ createSequelizeSiglumeOrderStore,
197
+ createDrizzleSiglumeOrderStore,
198
+ } from "./siglume/siglume-order-store.sql.js";
199
+
200
+ const order_store = createPrismaSiglumeOrderStore(prisma, {
201
+ dialect: "postgres",
202
+ orders_table: "orders",
203
+ order_id_column: "id",
204
+ amount_minor_column: "amount_minor",
205
+ currency_column: "currency",
206
+ });
207
+ ```
208
+
209
+ FastAPI:
210
+
211
+ ```py
212
+ from sqlalchemy.orm import sessionmaker
213
+ from .siglume.siglume_order_store_sqlalchemy import (
214
+ SQLAlchemySiglumeOrderStore,
215
+ create_sqlalchemy_engine,
216
+ create_sqlalchemy_siglume_schema,
217
+ )
218
+
219
+ engine = create_sqlalchemy_engine(os.environ["DATABASE_URL"])
220
+ create_sqlalchemy_siglume_schema(engine)
221
+ SessionLocal = sessionmaker(engine, future=True)
222
+ order_store = SQLAlchemySiglumeOrderStore(SessionLocal)
223
+ ```
224
+
225
+ The adapters persist one checkout attempt per order, reuse the checkout URL on
226
+ retries, record webhook event ids only after the order update/review write
227
+ succeeds, and keep duplicate deliveries from double-fulfilling an order.
228
+
229
+ ## 6. Start checkout from your frontend
121
230
 
122
231
  Call your own server route:
123
232
 
@@ -129,16 +238,17 @@ curl -X POST https://api.your-product.example/payments/checkout/siglume/start \
129
238
 
130
239
  Redirect the shopper to the returned `checkout_url`.
131
240
 
132
- ## 5. Done means
241
+ ## 7. Done means
133
242
 
134
243
  Your product is integrated when:
135
244
 
136
- - `npx siglume-check readiness` passes,
245
+ - `npx siglume-check readiness --sandbox` passes against your local product,
246
+ - `npx siglume-check readiness` passes against live Siglume credentials,
137
247
  - your product has mounted checkout and webhook routes,
138
- - your order database stores `challenge_hash` for the order,
248
+ - your order database uses the SQL/ORM adapter or an equivalent transactional store,
139
249
  - the signed webhook verifies against the raw body,
140
250
  - `standard_settled` marks the order paid once,
141
- - `metered_usage_accepted` uses a separate fulfilled-but-unsettled state.
251
+ - a failed webhook handler is retried and duplicate webhook deliveries do not double-fulfill the order.
142
252
 
143
253
  For Micro / Nano revenue reconciliation, read
144
254
  [Payment lifecycle](./payment-lifecycle.md) and
@@ -0,0 +1,60 @@
1
+ # SDRP Sandbox
2
+
3
+ Use the SDK sandbox before live Siglume credentials. It is a local
4
+ Siglume-compatible API server for product integration testing. It creates fake
5
+ checkout sessions, signs `direct_payment.confirmed` webhooks, records delivery
6
+ status, and never charges a wallet.
7
+
8
+ Start your product locally first, then run:
9
+
10
+ ```bash
11
+ npx siglume-sdrp sandbox \
12
+ --origin http://localhost:3000 \
13
+ --webhook-url http://localhost:3000/payments/webhooks/siglume
14
+ ```
15
+
16
+ Set the printed environment variables in your product:
17
+
18
+ ```bash
19
+ SIGLUME_ENV=sandbox
20
+ SIGLUME_API_BASE=http://127.0.0.1:8787/v1
21
+ SIGLUME_MERCHANT_AUTH_TOKEN=sandbox_merchant_token
22
+ SIGLUME_DIRECT_PAYMENT_MERCHANT=sandbox_merchant
23
+ SHOP_PUBLIC_ORIGIN=http://localhost:3000
24
+ SHOP_WEBHOOK_URL=http://localhost:3000/payments/webhooks/siglume
25
+ SIGLUME_WEBHOOK_SECRET=whsec_sandbox_local
26
+ ```
27
+
28
+ Then verify the integration:
29
+
30
+ ```bash
31
+ npx siglume-check readiness --sandbox
32
+ ```
33
+
34
+ Create a checkout through your own product route:
35
+
36
+ ```bash
37
+ curl -X POST http://localhost:3000/payments/checkout/siglume/start \
38
+ -H "content-type: application/json" \
39
+ -d "{\"order_id\":\"order_123\"}"
40
+ ```
41
+
42
+ Open the returned `checkout_url` and click the sandbox confirm button. Your
43
+ product should receive a signed webhook and mark the Standard order paid once.
44
+
45
+ Sandbox Micro / Nano behavior follows the same public classifications:
46
+
47
+ - JPY 501+ / USD 3.01+ returns `standard_settled`.
48
+ - JPY 50-500 / USD 0.31-3.00 returns `metered_usage_accepted` with weekly Micro settlement fields.
49
+ - JPY 1-49 / USD 0.01-0.30 returns `metered_usage_accepted` with monthly Nano settlement fields.
50
+
51
+ The generated route defaults to Standard-only, so Micro / Nano checkout returns
52
+ `METERED_INTEGRATION_REQUIRED` until you explicitly enable metered handling.
53
+
54
+ Before live launch:
55
+
56
+ - run `npx siglume-check readiness --sandbox` against the local product,
57
+ - run the same checkout path and confirm a sandbox webhook,
58
+ - switch to live `SIGLUME_MERCHANT_AUTH_TOKEN`, `SIGLUME_DIRECT_PAYMENT_MERCHANT`, `SHOP_PUBLIC_ORIGIN`, `SHOP_WEBHOOK_URL`, and `SIGLUME_WEBHOOK_SECRET`,
59
+ - run `npx siglume-check readiness` without `--sandbox`,
60
+ - confirm the live webhook subscription and signing secret are for the live product URL.