@siglume/direct-request-payment 0.4.17 → 0.4.19

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 (35) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +60 -4
  3. package/bin/siglume-sdrp.mjs +267 -0
  4. package/dist/index.cjs +3 -3
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +3 -3
  7. package/dist/index.d.ts +3 -3
  8. package/dist/index.js +3 -3
  9. package/dist/index.js.map +1 -1
  10. package/docs/announcement-ja.md +5 -3
  11. package/docs/api-reference.md +34 -1
  12. package/docs/merchant-quickstart.md +19 -2
  13. package/docs/payment-lifecycle.md +85 -0
  14. package/docs/pricing.md +6 -6
  15. package/docs/quickstart-10-minutes.md +145 -0
  16. package/docs/troubleshooting.md +70 -0
  17. package/examples/express-checkout.ts +14 -0
  18. package/examples/hosted-checkout-python/.env.example +5 -0
  19. package/examples/hosted-checkout-python/README.md +21 -0
  20. package/examples/hosted-checkout-python/app.py +124 -0
  21. package/examples/hosted-checkout-python/order_store.py +42 -0
  22. package/examples/hosted-checkout-python/pyproject.toml +9 -0
  23. package/examples/hosted-checkout-typescript/.env.example +5 -0
  24. package/examples/hosted-checkout-typescript/README.md +21 -0
  25. package/examples/hosted-checkout-typescript/package.json +20 -0
  26. package/examples/hosted-checkout-typescript/src/order-store.ts +52 -0
  27. package/examples/hosted-checkout-typescript/src/server.ts +139 -0
  28. package/examples/hosted-checkout-typescript/tsconfig.json +13 -0
  29. package/package.json +12 -1
  30. package/templates/express/README.md +22 -0
  31. package/templates/express/siglume-order-store.example.ts +53 -0
  32. package/templates/express/siglume-sdrp-routes.ts +157 -0
  33. package/templates/fastapi/README.md +22 -0
  34. package/templates/fastapi/siglume_order_store_example.py +54 -0
  35. package/templates/fastapi/siglume_sdrp_routes.py +107 -0
@@ -3,6 +3,12 @@
3
3
  This guide shows the minimum safe Siglume Direct Request Payment flow for an
4
4
  external merchant.
5
5
 
6
+ For the shortest existing-product integration path, use
7
+ [10-Minute Product Integration](./quickstart-10-minutes.md). That guide copies
8
+ checkout and webhook routes into an Express or FastAPI product and verifies
9
+ Hosted Checkout readiness before coding. This merchant quickstart is broader
10
+ and includes the agent/API path plus Micro / Nano reconciliation notes.
11
+
6
12
  ## Actors
7
13
 
8
14
  - Merchant server: owns the order, amount, currency, challenge secret, webhook
@@ -44,8 +50,8 @@ There are two ways a buyer reaches you, and you integrate each differently:
44
50
 
45
51
  - **Human web shopper → Hosted Checkout (Beta; server rollout in progress).** Create a checkout session and
46
52
  redirect the shopper to the Siglume-hosted page (the
47
- [section below](#hosted-checkout-human-web-shoppers)). This is the path that
48
- resembles a Stripe-style hosted checkout.
53
+ [section below](#hosted-checkout-human-web-shoppers)). This is the Siglume
54
+ wallet hosted checkout path for human web shoppers.
49
55
  - **AI agent / agent-to-agent (AtoA) → direct API / tools.** An autonomous
50
56
  buyer pays through `DirectRequestPaymentClient` or the marketplace tool
51
57
  `market_confirm_direct_payment_and_execute`, as in sections 2-4 below.
@@ -59,6 +65,10 @@ the merchant SDK never authenticates the buyer, and you fulfill on the same
59
65
  **Beta / server rollout:** Hosted Checkout is rolling out account by account.
60
66
  Some merchant accounts may not have the server endpoint enabled yet. The SDK
61
67
  raises `HostedCheckoutNotAvailableError` for rollout 404/409 responses.
68
+ Confirm readiness before building the flow; see
69
+ [Hosted Checkout readiness](./troubleshooting.md#hosted-checkout-readiness).
70
+ If the account is not enabled, do not continue with a human web checkout until
71
+ Siglume enables it for that merchant account.
62
72
 
63
73
  When a person clicks "Pay with Siglume" on your site, create a session and
64
74
  redirect them to the returned `checkout_url`. They sign into Siglume on the
@@ -600,9 +610,16 @@ Do not book Micro / Nano provider revenue as settled revenue until the batch is
600
610
  `settled` and `chain_receipt_id` is present. See
601
611
  [Micro / Nano Statements and Notices](./metered-statements.md) for the full
602
612
  manual, including buyer past-due blocks and public failure fields.
613
+ For a compact state-machine view across Standard, Micro, and Nano, see
614
+ [Payment lifecycle](./payment-lifecycle.md).
603
615
 
604
616
  ## Failure Handling
605
617
 
618
+ For retry policy, buyer-safe copy, webhook signature failures, Hosted Checkout
619
+ readiness, and support escalation, see
620
+ [Troubleshooting](./troubleshooting.md). The short list below is only the common
621
+ payment-domain errors.
622
+
606
623
  - `EXTERNAL_402_CHALLENGE_REQUIRED`: the merchant server did not provide a
607
624
  challenge.
608
625
  - `INVALID_EXTERNAL_402_CHALLENGE`: the amount, currency, merchant, nonce, or
@@ -0,0 +1,85 @@
1
+ # Payment Lifecycle
2
+
3
+ This page separates three ideas that are easy to mix up:
4
+
5
+ - **checkout status**: the shopper's Hosted Checkout session state,
6
+ - **merchant fulfillment state**: whether your system can deliver the order,
7
+ - **provider revenue state**: whether the provider has settled revenue.
8
+
9
+ ## Standard Payment
10
+
11
+ ```text
12
+ checkout open
13
+ -> buyer authenticates
14
+ -> buyer pays
15
+ -> direct_payment.confirmed webhook
16
+ -> classifier kind: standard_settled
17
+ -> merchant marks order paid once
18
+ -> provider revenue is settled
19
+ ```
20
+
21
+ For Standard Payment, fulfill only after a signed
22
+ `direct_payment.confirmed` webhook verifies and
23
+ `classifyDirectPaymentConfirmation(event)` returns `standard_settled`. That
24
+ classification requires Standard pricing, per-payment on-chain finality, settled
25
+ status, non-empty `requirement_id`, non-empty `challenge_hash`, and non-empty
26
+ `chain_receipt_id`.
27
+
28
+ ## Micro / Nano Payment
29
+
30
+ ```text
31
+ checkout open or agent/API payment starts
32
+ -> usage accepted
33
+ -> direct_payment.confirmed webhook
34
+ -> classifier kind: metered_usage_accepted
35
+ -> merchant may fulfill as fulfilled_unsettled
36
+ -> open period closes by amount threshold or schedule
37
+ -> final notice window
38
+ -> submitted / retrying / past_due if needed
39
+ -> aggregated on-chain settlement
40
+ -> classifier kind: metered_batch_settled
41
+ -> provider revenue is settled
42
+ ```
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`.
48
+
49
+ ## Field meanings
50
+
51
+ | Field or state | What it means | What it does not mean |
52
+ | --- | --- | --- |
53
+ | Hosted Checkout `status: "paid"` | The checkout session accepted the wallet payment flow. | For Micro / Nano, it does not mean provider revenue is settled. |
54
+ | `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. |
56
+ | `fulfilled_unsettled` | Your merchant system delivered the item before Micro / Nano settlement. | It is not a Siglume settlement status. |
57
+ | `metered_batch_settled` | Aggregated Micro / Nano batch settled on-chain. | It does not identify one order by challenge hash. |
58
+ | `pending_settlement` | Micro / Nano usage is waiting for aggregated settlement. | It is not a failure by itself. |
59
+ | `past_due` | Settlement failed or remains unresolved after retry state. | It is not collected revenue. |
60
+ | `uncollectible` / `written_off` | Operator terminal resolution after past-due review. | It is not settled, unsettled, or past-due revenue. |
61
+
62
+ ## Fulfillment rules
63
+
64
+ - Use the webhook raw body and `Siglume-Signature`; do not verify a
65
+ re-stringified JSON object.
66
+ - Store `challenge_hash` on the order before redirecting the buyer.
67
+ - 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.
71
+ - Treat `unknown` classifications as manual review. Do not mark paid or
72
+ fulfilled from the event name alone.
73
+
74
+ ## Revenue rules
75
+
76
+ - Standard revenue is settled when the payment confirms on-chain.
77
+ - Micro / Nano buyer debit is seller-borne-fee safe:
78
+ `buyer_debit_minor = provider_gross_amount_minor`.
79
+ - Micro / Nano provider receivable is
80
+ `provider_gross_amount_minor - protocol_fee_minor`.
81
+ - Micro / Nano revenue is settled only after the aggregated batch is settled
82
+ on-chain.
83
+ - If `total_unsettled_exposure_minor` for the same buyer / provider / token /
84
+ pricing band is at or above the fixed threshold, new Micro / Nano usage is
85
+ paused until settlement succeeds or an operator resolves the state.
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.17. Pricing can change by agreement or future product
4
+ Payment as of SDK v0.4.19. 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
 
@@ -192,11 +192,11 @@ once provider gross reaches JPY 10,000 or USD 100.00. These are fixed
192
192
  market-specific thresholds, not FX conversions of one another. A payment is
193
193
  final only after its on-chain settlement confirms. Micro and Nano are automatic
194
194
  amount bands, not customer-selected options. The account-assigned period
195
- boundaries, roughly 3-day pre-debit notice window, early settlement threshold,
196
- and current retry policy above are the public behavior as of 2026-06-19. Treat
197
- the platform's statement status, `not_before_attempt_at`, Standard `fee_bps`,
198
- and Micro / Nano statement amount fields as authoritative rather than
199
- hard-coding local revenue recognition.
195
+ boundaries, roughly 3-day pre-debit notice window, and current retry policy above
196
+ are platform-managed public behavior as of 2026-06-19. Treat the platform's
197
+ statement status, `not_before_attempt_at`, Standard `fee_bps`, and Micro / Nano
198
+ statement amount fields as authoritative rather than hard-coding local revenue
199
+ recognition.
200
200
 
201
201
  ## Micro / Nano Seller-borne Amounts
202
202
 
@@ -0,0 +1,145 @@
1
+ # 10-Minute Product Integration
2
+
3
+ This guide is the supported 10-minute path for adding SDRP Hosted Checkout to
4
+ an existing product. The goal is to
5
+ add two routes to your own server:
6
+
7
+ - `POST /payments/checkout/siglume/start`
8
+ - `POST /payments/webhooks/siglume`
9
+
10
+ The SDK supplies the readiness check, route files, webhook verification, payment
11
+ classification, and the order-store adapter contract. Your app supplies the
12
+ real order lookup and fulfillment writes.
13
+
14
+ ## 0. Readiness first
15
+
16
+ Install the SDK in your product.
17
+
18
+ Node / Express:
19
+
20
+ ```bash
21
+ npm install @siglume/direct-request-payment
22
+ ```
23
+
24
+ Python / FastAPI:
25
+
26
+ ```bash
27
+ pip install siglume-direct-request-payment
28
+ ```
29
+
30
+ Set these environment variables in your app or `.env`:
31
+
32
+ ```bash
33
+ SIGLUME_MERCHANT_AUTH_TOKEN=<merchant Siglume bearer token>
34
+ SIGLUME_DIRECT_PAYMENT_MERCHANT=<merchant key>
35
+ SHOP_PUBLIC_ORIGIN=https://www.your-product.example
36
+ SHOP_WEBHOOK_URL=https://api.your-product.example/payments/webhooks/siglume
37
+ ```
38
+
39
+ Then run:
40
+
41
+ ```bash
42
+ npx siglume-check readiness
43
+ ```
44
+
45
+ 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.
49
+
50
+ For CI or a preflight script:
51
+
52
+ ```bash
53
+ npx siglume-check readiness --json
54
+ ```
55
+
56
+ If this command fails, fix the reported item first. Do not build a human web
57
+ checkout path until readiness passes.
58
+
59
+ ## 1. Copy integration files into your product
60
+
61
+ For Express:
62
+
63
+ ```bash
64
+ npx siglume-sdrp init express --target src/siglume
65
+ ```
66
+
67
+ For FastAPI:
68
+
69
+ ```bash
70
+ siglume-sdrp init fastapi --target app/siglume
71
+ ```
72
+
73
+ These commands copy framework-specific route files into your codebase. The
74
+ generated files are intentionally small and are meant to be edited.
75
+
76
+ ## 2. Mount the routes
77
+
78
+ Express:
79
+
80
+ ```ts
81
+ import { createSiglumeSdrpRouter } from "./siglume/siglume-sdrp-routes.js";
82
+ import { siglumeOrderStore } from "./siglume/siglume-order-store.example.js";
83
+
84
+ app.use("/payments", createSiglumeSdrpRouter({
85
+ merchant: process.env.SIGLUME_DIRECT_PAYMENT_MERCHANT!,
86
+ merchant_auth_token: process.env.SIGLUME_MERCHANT_AUTH_TOKEN!,
87
+ webhook_secret: process.env.SIGLUME_WEBHOOK_SECRET!,
88
+ shop_public_origin: process.env.SHOP_PUBLIC_ORIGIN!,
89
+ order_store: siglumeOrderStore,
90
+ }));
91
+ ```
92
+
93
+ FastAPI:
94
+
95
+ ```py
96
+ from .siglume.siglume_order_store_example import ExampleSiglumeOrderStore
97
+ from .siglume.siglume_sdrp_routes import create_siglume_sdrp_router
98
+
99
+ app.include_router(
100
+ create_siglume_sdrp_router(ExampleSiglumeOrderStore()),
101
+ prefix="/payments",
102
+ )
103
+ ```
104
+
105
+ ## 3. Replace the order-store example
106
+
107
+ Replace the example store with your product's order database. The adapter must:
108
+
109
+ - load the order by your `order_id`,
110
+ - verify the current user is allowed to pay for that order,
111
+ - return the server-authored `amount_minor` and `currency`,
112
+ - persist `challenge_hash` and `checkout_session_id` before redirecting,
113
+ - record webhook event ids durably,
114
+ - mark Standard orders paid exactly once,
115
+ - mark Micro / Nano orders as fulfilled but unsettled exactly once,
116
+ - route unknown classifications to manual review.
117
+
118
+ Do not calculate the amount from browser input.
119
+
120
+ ## 4. Start checkout from your frontend
121
+
122
+ Call your own server route:
123
+
124
+ ```bash
125
+ curl -X POST https://api.your-product.example/payments/checkout/siglume/start \
126
+ -H "content-type: application/json" \
127
+ -d "{\"order_id\":\"order_123\"}"
128
+ ```
129
+
130
+ Redirect the shopper to the returned `checkout_url`.
131
+
132
+ ## 5. Done means
133
+
134
+ Your product is integrated when:
135
+
136
+ - `npx siglume-check readiness` passes,
137
+ - your product has mounted checkout and webhook routes,
138
+ - your order database stores `challenge_hash` for the order,
139
+ - the signed webhook verifies against the raw body,
140
+ - `standard_settled` marks the order paid once,
141
+ - `metered_usage_accepted` uses a separate fulfilled-but-unsettled state.
142
+
143
+ For Micro / Nano revenue reconciliation, read
144
+ [Payment lifecycle](./payment-lifecycle.md) and
145
+ [Micro / Nano Statements and Notices](./metered-statements.md).
@@ -0,0 +1,70 @@
1
+ # Troubleshooting
2
+
3
+ Use this page when an integration fails before, during, or after checkout.
4
+ Where a Siglume API response includes `request_id`, `trace_id`, or
5
+ `support_reference`, include that value when contacting Siglume support or your
6
+ Siglume account contact.
7
+
8
+ ## Hosted Checkout readiness
9
+
10
+ Hosted Checkout is enabled account by account during beta. Check this before
11
+ building a human web checkout:
12
+
13
+ ```bash
14
+ npx siglume-check readiness
15
+ ```
16
+
17
+ The command validates local configuration, reads the merchant account, and
18
+ creates one unpaid expiring checkout session to prove Hosted Checkout is
19
+ available for this merchant account.
20
+
21
+ - The merchant account exists.
22
+ - The merchant billing mandate is active.
23
+ - The webhook callback URL is HTTPS and reachable.
24
+ - The checkout return URL origins are registered through
25
+ `checkout_allowed_origins`.
26
+ - The account has Hosted Checkout enabled.
27
+
28
+ If `createCheckoutSession(...)` or `getCheckoutSession(...)` raises
29
+ `HostedCheckoutNotAvailableError`, do not show the raw 404/409 to the buyer.
30
+ Stop the human checkout flow and contact Siglume support or your Siglume account
31
+ contact for Hosted Checkout enablement.
32
+
33
+ ## API errors
34
+
35
+ | Status / code | Likely cause | Retry? | Same idempotency key? | Buyer copy | Operator action |
36
+ | --- | --- | --- | --- | --- | --- |
37
+ | `401` / `403` | Missing token, expired token, wrong account, or insufficient scope. | No, not until credentials are fixed. | n/a | "Payment setup needs attention. Please try later." | Check whether you used a merchant Siglume bearer token for merchant setup and a buyer/provider Siglume bearer token for buyer/provider APIs. Do not use `cli_` keys. |
38
+ | `404` / `409` Hosted Checkout rollout | Hosted Checkout is not enabled for this merchant account. | No. | n/a | "This payment method is not available for this store yet." | Contact Siglume for enablement; use agent/API only if that is actually your buyer flow. |
39
+ | `409` idempotency conflict | The same idempotency key was reused with different Micro / Nano input. | No. | Do not reuse with different payload. | "This payment attempt could not be completed. Please retry from the order page." | Create a new payment attempt nonce/key for the changed order. |
40
+ | `422` validation error | Invalid amount, currency, nonce, URL, origin, or metadata shape. | No, fix input. | n/a | "Payment information is invalid. Please refresh and retry." | Validate server-side amount/currency and registered URL origins. |
41
+ | `429` | Rate limit. | Yes, after `Retry-After` when present. | Reuse only for the same logical attempt and same payload. | "Payment is busy. Please retry shortly." | Back off; do not create many new payment attempts. |
42
+ | `5xx` or timeout | Temporary Siglume or network failure. | Yes, with bounded exponential backoff. | Reuse for the same logical attempt and same payload. | "Payment is temporarily unavailable. Please retry shortly." | Log request identifiers; avoid fulfilling without a verified webhook. |
43
+ | `METERED_SETTLEMENT_PAST_DUE` | Micro / Nano usage is paused because unsettled exposure or a past-due batch remains unresolved for the same buyer / provider / token / pricing band. | No, until settlement succeeds or is resolved. | n/a | "This low-value payment is paused until previous settlement completes." | Check statement APIs and `support_reference`; do not call the provider API. |
44
+
45
+ ## Webhook failures
46
+
47
+ - Verify the exact raw request body bytes or raw body string.
48
+ - Do not verify a parsed JSON object or a re-stringified JSON body.
49
+ - Return a 2xx only after you have durably recorded the event or safely decided
50
+ it is duplicate/ignored.
51
+ - Store processed webhook event ids or settlement identifiers durably; an
52
+ in-memory set is not enough for production.
53
+ - Do not assume delivery order. A settlement batch event may be reconciled from
54
+ statement APIs rather than from one order challenge.
55
+ - On signature failure, return a non-2xx status and do not mutate order state.
56
+ - On a valid but unknown payment classification, return 2xx only after routing
57
+ it to durable manual review.
58
+
59
+ ## Refunds and adjustments
60
+
61
+ This SDK release does not expose a self-service refund API. For Standard
62
+ Payment refunds or Micro / Nano adjustments, use the explicit Siglume support or
63
+ platform process available to your account. Do not reverse settled revenue by
64
+ editing local statements or CSV exports.
65
+
66
+ ## Safe buyer messages
67
+
68
+ Keep buyer-facing messages short and non-diagnostic. Do not expose raw API
69
+ errors, wallet internals, RPC URLs, stack traces, webhook secrets, or support
70
+ references. Log detailed context server-side with request identifiers.
@@ -1,3 +1,17 @@
1
+ /**
2
+ * DEMO ONLY.
3
+ *
4
+ * This file shows the minimum Hosted Checkout webhook shape, but it is not
5
+ * production-safe:
6
+ * - in-memory order storage
7
+ * - no buyer authentication or order ownership checks
8
+ * - no database transaction around fulfillment
9
+ * - no durable webhook event deduplication
10
+ * - no production refund or support workflow
11
+ *
12
+ * For production, persist orders and processed webhook ids in a database,
13
+ * authorize every checkout start request, and make fulfillment idempotent.
14
+ */
1
15
  import express from "express";
2
16
  import {
3
17
  classifyDirectPaymentConfirmation,
@@ -0,0 +1,5 @@
1
+ SIGLUME_MERCHANT_AUTH_TOKEN=
2
+ SIGLUME_DIRECT_PAYMENT_MERCHANT=example_merchant
3
+ SIGLUME_WEBHOOK_SECRET=
4
+ SHOP_PUBLIC_ORIGIN=https://www.example.com
5
+ PORT=3000
@@ -0,0 +1,21 @@
1
+ # Hosted Checkout Python Starter
2
+
3
+ Minimal Flask starter for one Standard Payment test order.
4
+
5
+ ```bash
6
+ cp .env.example .env
7
+ python -m pip install -e . siglume-direct-request-payment
8
+ python app.py
9
+ ```
10
+
11
+ Then call:
12
+
13
+ ```bash
14
+ curl -X POST http://localhost:3000/checkout/siglume/start \
15
+ -H "content-type: application/json" \
16
+ -d "{\"order_id\":\"order_123\"}"
17
+ ```
18
+
19
+ This starter uses in-memory storage so it is easy to inspect. Replace it with a
20
+ database before production. Production systems must persist orders, processed
21
+ webhook event ids, and fulfillment state in one durable transaction.
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from dotenv import load_dotenv
6
+ from flask import Flask, jsonify, request
7
+ from siglume_direct_request_payment import (
8
+ DirectRequestPaymentMerchantClient,
9
+ HostedCheckoutNotAvailableError,
10
+ classify_direct_payment_confirmation,
11
+ verify_direct_request_payment_webhook,
12
+ )
13
+
14
+ from order_store import (
15
+ all_orders,
16
+ find_order_by_challenge_hash,
17
+ get_order,
18
+ mark_webhook_event_processed_once,
19
+ save_order,
20
+ )
21
+
22
+ load_dotenv()
23
+
24
+ app = Flask(__name__)
25
+ merchant_key = os.environ.get("SIGLUME_DIRECT_PAYMENT_MERCHANT", "example_merchant")
26
+ shop_origin = os.environ.get("SHOP_PUBLIC_ORIGIN", "https://www.example.com")
27
+ siglume_merchant = DirectRequestPaymentMerchantClient(
28
+ auth_token=os.environ.get("SIGLUME_MERCHANT_AUTH_TOKEN"),
29
+ )
30
+
31
+
32
+ @app.get("/orders")
33
+ def orders():
34
+ return jsonify({"orders": all_orders()})
35
+
36
+
37
+ @app.post("/checkout/siglume/start")
38
+ def start_checkout():
39
+ order_id = str((request.get_json(silent=True) or {}).get("order_id") or "")
40
+ order = get_order(order_id)
41
+ if order is None:
42
+ return jsonify({"error": "order_not_found"}), 404
43
+
44
+ order["payment_attempt"] = int(order.get("payment_attempt") or 0) + 1
45
+ session = siglume_merchant.create_checkout_session(
46
+ merchant=merchant_key,
47
+ amount_minor=int(order["amount_minor"]),
48
+ currency=str(order["currency"]),
49
+ nonce=f"{order['id']}-attempt_{order['payment_attempt']}",
50
+ success_url=f"{shop_origin}/thanks",
51
+ cancel_url=f"{shop_origin}/cart",
52
+ metadata={"order_id": order["id"]},
53
+ )
54
+
55
+ order["siglume_challenge_hash"] = session["challenge_hash"]
56
+ order["siglume_checkout_session_id"] = session["session_id"]
57
+ order["siglume_payment_status"] = "pending"
58
+ save_order(order)
59
+
60
+ return jsonify(
61
+ {
62
+ "order_id": order["id"],
63
+ "amount_minor": order["amount_minor"],
64
+ "currency": order["currency"],
65
+ "checkout_url": session["checkout_url"],
66
+ "session_id": session["session_id"],
67
+ }
68
+ )
69
+
70
+
71
+ @app.post("/siglume/webhook")
72
+ def siglume_webhook():
73
+ verified = verify_direct_request_payment_webhook(
74
+ os.environ.get("SIGLUME_WEBHOOK_SECRET", ""),
75
+ request.get_data(),
76
+ request.headers.get("Siglume-Signature", ""),
77
+ )
78
+ event = verified["event"]
79
+
80
+ if not mark_webhook_event_processed_once(str(event["id"])):
81
+ return "", 204
82
+
83
+ if event["type"] == "direct_payment.confirmed":
84
+ confirmation = classify_direct_payment_confirmation(event)
85
+
86
+ if confirmation["kind"] == "standard_settled":
87
+ order = find_order_by_challenge_hash(confirmation["challenge_hash"])
88
+ if order is not None:
89
+ order["siglume_payment_status"] = "paid"
90
+ order["siglume_requirement_id"] = confirmation["requirement_id"]
91
+ order["siglume_chain_receipt_id"] = confirmation["chain_receipt_id"]
92
+ save_order(order)
93
+ elif confirmation["kind"] == "metered_usage_accepted":
94
+ order = find_order_by_challenge_hash(confirmation["challenge_hash"])
95
+ if order is not None:
96
+ order["siglume_payment_status"] = "fulfilled_unsettled"
97
+ order["siglume_requirement_id"] = confirmation["requirement_id"]
98
+ save_order(order)
99
+ else:
100
+ app.logger.warning(
101
+ "manual payment review required",
102
+ extra={
103
+ "event_id": event["id"],
104
+ "reason": confirmation.get("reason"),
105
+ "requirement_id": confirmation.get("requirement_id"),
106
+ },
107
+ )
108
+
109
+ return "", 204
110
+
111
+
112
+ @app.errorhandler(HostedCheckoutNotAvailableError)
113
+ def hosted_checkout_not_available(_: HostedCheckoutNotAvailableError):
114
+ return jsonify({"error": "hosted_checkout_not_enabled"}), 409
115
+
116
+
117
+ @app.errorhandler(Exception)
118
+ def internal_error(error: Exception):
119
+ app.logger.error("checkout starter error", extra={"name": type(error).__name__})
120
+ return jsonify({"error": "internal_error"}), 500
121
+
122
+
123
+ if __name__ == "__main__":
124
+ app.run(host="0.0.0.0", port=int(os.environ.get("PORT", "3000")))
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ Order = dict[str, Any]
6
+
7
+ _orders: dict[str, Order] = {
8
+ "order_123": {
9
+ "id": "order_123",
10
+ "amount_minor": 1200,
11
+ "currency": "JPY",
12
+ "payment_attempt": 0,
13
+ "siglume_payment_status": "created",
14
+ }
15
+ }
16
+ _processed_webhook_events: set[str] = set()
17
+
18
+
19
+ def get_order(order_id: str) -> Order | None:
20
+ return _orders.get(order_id)
21
+
22
+
23
+ def all_orders() -> list[Order]:
24
+ return list(_orders.values())
25
+
26
+
27
+ def save_order(order: Order) -> None:
28
+ _orders[str(order["id"])] = order
29
+
30
+
31
+ def find_order_by_challenge_hash(challenge_hash: str) -> Order | None:
32
+ for order in _orders.values():
33
+ if order.get("siglume_challenge_hash") == challenge_hash:
34
+ return order
35
+ return None
36
+
37
+
38
+ def mark_webhook_event_processed_once(event_id: str) -> bool:
39
+ if event_id in _processed_webhook_events:
40
+ return False
41
+ _processed_webhook_events.add(event_id)
42
+ return True
@@ -0,0 +1,9 @@
1
+ [project]
2
+ name = "siglume-hosted-checkout-python-starter"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.11"
5
+ dependencies = [
6
+ "Flask>=3.0,<4",
7
+ "python-dotenv>=1.0,<2",
8
+ "siglume-direct-request-payment>=0.4.19,<0.5",
9
+ ]
@@ -0,0 +1,5 @@
1
+ SIGLUME_MERCHANT_AUTH_TOKEN=
2
+ SIGLUME_DIRECT_PAYMENT_MERCHANT=example_merchant
3
+ SIGLUME_WEBHOOK_SECRET=
4
+ SHOP_PUBLIC_ORIGIN=https://www.example.com
5
+ PORT=3000
@@ -0,0 +1,21 @@
1
+ # Hosted Checkout TypeScript Starter
2
+
3
+ Minimal Express starter for one Standard Payment test order.
4
+
5
+ ```bash
6
+ cp .env.example .env
7
+ npm install
8
+ npm run dev
9
+ ```
10
+
11
+ Then call:
12
+
13
+ ```bash
14
+ curl -X POST http://localhost:3000/checkout/siglume/start \
15
+ -H "content-type: application/json" \
16
+ -d "{\"order_id\":\"order_123\"}"
17
+ ```
18
+
19
+ This starter uses in-memory storage so it is easy to inspect. Replace it with a
20
+ database before production. Production systems must persist orders, processed
21
+ webhook event ids, and fulfillment state in one durable transaction.
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "siglume-hosted-checkout-typescript-starter",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "tsx src/server.ts",
7
+ "typecheck": "tsc --noEmit"
8
+ },
9
+ "dependencies": {
10
+ "@siglume/direct-request-payment": "file:../..",
11
+ "dotenv": "^16.4.7",
12
+ "express": "^4.21.2"
13
+ },
14
+ "devDependencies": {
15
+ "@types/express": "^4.17.21",
16
+ "@types/node": "^20.16.5",
17
+ "tsx": "^4.19.2",
18
+ "typescript": "^5.6.3"
19
+ }
20
+ }