@siglume/direct-request-payment 0.4.17 → 0.4.18

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.
@@ -3,6 +3,13 @@
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 first-test path, use
7
+ [10-Minute First Test Payment](./quickstart-10-minutes.md). That guide covers
8
+ only one Standard Payment test after account, Hosted Checkout, billing mandate,
9
+ HTTPS webhook, and buyer wallet prerequisites are ready. This merchant
10
+ quickstart is broader and includes the agent/API path plus Micro / Nano
11
+ reconciliation notes.
12
+
6
13
  ## Actors
7
14
 
8
15
  - Merchant server: owns the order, amount, currency, challenge secret, webhook
@@ -44,8 +51,8 @@ There are two ways a buyer reaches you, and you integrate each differently:
44
51
 
45
52
  - **Human web shopper → Hosted Checkout (Beta; server rollout in progress).** Create a checkout session and
46
53
  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.
54
+ [section below](#hosted-checkout-human-web-shoppers)). This is the Siglume
55
+ wallet hosted checkout path for human web shoppers.
49
56
  - **AI agent / agent-to-agent (AtoA) → direct API / tools.** An autonomous
50
57
  buyer pays through `DirectRequestPaymentClient` or the marketplace tool
51
58
  `market_confirm_direct_payment_and_execute`, as in sections 2-4 below.
@@ -59,6 +66,10 @@ the merchant SDK never authenticates the buyer, and you fulfill on the same
59
66
  **Beta / server rollout:** Hosted Checkout is rolling out account by account.
60
67
  Some merchant accounts may not have the server endpoint enabled yet. The SDK
61
68
  raises `HostedCheckoutNotAvailableError` for rollout 404/409 responses.
69
+ Confirm readiness before building the flow; see
70
+ [Hosted Checkout readiness](./troubleshooting.md#hosted-checkout-readiness).
71
+ If the account is not enabled, do not continue with a human web checkout until
72
+ Siglume enables it for that merchant account.
62
73
 
63
74
  When a person clicks "Pay with Siglume" on your site, create a session and
64
75
  redirect them to the returned `checkout_url`. They sign into Siglume on the
@@ -600,9 +611,16 @@ Do not book Micro / Nano provider revenue as settled revenue until the batch is
600
611
  `settled` and `chain_receipt_id` is present. See
601
612
  [Micro / Nano Statements and Notices](./metered-statements.md) for the full
602
613
  manual, including buyer past-due blocks and public failure fields.
614
+ For a compact state-machine view across Standard, Micro, and Nano, see
615
+ [Payment lifecycle](./payment-lifecycle.md).
603
616
 
604
617
  ## Failure Handling
605
618
 
619
+ For retry policy, buyer-safe copy, webhook signature failures, Hosted Checkout
620
+ readiness, and support escalation, see
621
+ [Troubleshooting](./troubleshooting.md). The short list below is only the common
622
+ payment-domain errors.
623
+
606
624
  - `EXTERNAL_402_CHALLENGE_REQUIRED`: the merchant server did not provide a
607
625
  challenge.
608
626
  - `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.18. 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,176 @@
1
+ # 10-Minute First Test Payment
2
+
3
+ This guide is the shortest supported path to one **Standard Payment** test
4
+ through Siglume wallet Hosted Checkout. It is not a full production launch.
5
+
6
+ ## What the 10 minutes cover
7
+
8
+ You can count the 10 minutes only after the prerequisites below are already
9
+ ready. The target outcome is:
10
+
11
+ - one Standard-band order,
12
+ - one Hosted Checkout session,
13
+ - one signed `direct_payment.confirmed` webhook,
14
+ - one idempotent local fulfillment decision.
15
+
16
+ This guide does **not** cover production monitoring, refunds, subscriptions,
17
+ scheduled autopay, game entitlement recovery, or Micro / Nano accounting.
18
+
19
+ ## Prerequisites
20
+
21
+ Before starting, confirm:
22
+
23
+ - You have a Siglume merchant account and merchant Siglume bearer token.
24
+ - Hosted Checkout is enabled for that merchant account.
25
+ - The merchant billing mandate is active, including any required wallet
26
+ approval.
27
+ - You have a public HTTPS webhook URL that can receive the raw request body.
28
+ - Your checkout return URL origin is known and can be registered.
29
+ - The buyer has a Siglume wallet funded in the settlement token for the test
30
+ market: JPYC for JPY, USDC for USD.
31
+ - Your order amount is in the Standard band: JPY 501+ or USD 3.01+.
32
+
33
+ If Hosted Checkout is not enabled, stop here. The SDK raises
34
+ `HostedCheckoutNotAvailableError` for rollout 404/409 responses; contact
35
+ Siglume support or your Siglume account contact to enable the account before
36
+ continuing with a human web checkout.
37
+
38
+ ## 1. Install
39
+
40
+ Runnable starter directories are available if you want a small server to edit:
41
+
42
+ - [TypeScript Express starter](../examples/hosted-checkout-typescript)
43
+ - [Python Flask starter](../examples/hosted-checkout-python)
44
+
45
+ For an existing app, install the SDK directly:
46
+
47
+ ```bash
48
+ npm install @siglume/direct-request-payment
49
+ ```
50
+
51
+ or:
52
+
53
+ ```bash
54
+ pip install siglume-direct-request-payment
55
+ ```
56
+
57
+ ## 2. Set environment variables
58
+
59
+ ```bash
60
+ SIGLUME_MERCHANT_AUTH_TOKEN=<merchant Siglume bearer token>
61
+ SIGLUME_DIRECT_PAYMENT_MERCHANT=example_merchant
62
+ SHOP_PUBLIC_ORIGIN=https://www.example.com
63
+ SHOP_WEBHOOK_URL=https://api.example.com/siglume/webhook
64
+ ```
65
+
66
+ Do not use a Developer Portal `cli_` API key. Merchant setup requires the
67
+ merchant's Siglume bearer token.
68
+
69
+ ## 3. Register merchant settings
70
+
71
+ Run setup once from your server, CI, or integration machine:
72
+
73
+ ```ts
74
+ import { DirectRequestPaymentMerchantClient } from "@siglume/direct-request-payment";
75
+
76
+ const merchant = new DirectRequestPaymentMerchantClient({
77
+ auth_token: process.env.SIGLUME_MERCHANT_AUTH_TOKEN!,
78
+ });
79
+
80
+ const setup = await merchant.setupCheckout({
81
+ merchant: process.env.SIGLUME_DIRECT_PAYMENT_MERCHANT!,
82
+ display_name: "Example Merchant",
83
+ billing_plan: "launch",
84
+ billing_currency: "JPY",
85
+ webhook_callback_url: process.env.SHOP_WEBHOOK_URL!,
86
+ checkout_allowed_origins: [process.env.SHOP_PUBLIC_ORIGIN!],
87
+ });
88
+
89
+ console.log(setup.env.SIGLUME_DIRECT_PAYMENT_MERCHANT);
90
+ ```
91
+
92
+ Store the returned `SIGLUME_DIRECT_PAYMENT_CHALLENGE_SECRET` and
93
+ `SIGLUME_WEBHOOK_SECRET` in a server-side secret store. Secret values are
94
+ returned only when created or rotated.
95
+
96
+ ## 4. Create a Standard checkout session
97
+
98
+ For each order, create the order on your server first. Then create a Hosted
99
+ Checkout session:
100
+
101
+ ```ts
102
+ const session = await merchant.createCheckoutSession({
103
+ merchant: process.env.SIGLUME_DIRECT_PAYMENT_MERCHANT!,
104
+ amount_minor: 1200,
105
+ currency: "JPY",
106
+ nonce: "order_123-attempt_1",
107
+ success_url: `${process.env.SHOP_PUBLIC_ORIGIN}/thanks`,
108
+ cancel_url: `${process.env.SHOP_PUBLIC_ORIGIN}/cart`,
109
+ metadata: { order_id: "order_123" },
110
+ });
111
+
112
+ await orders.update("order_123", {
113
+ siglume_challenge_hash: session.challenge_hash,
114
+ siglume_checkout_session_id: session.session_id,
115
+ siglume_payment_status: "pending",
116
+ });
117
+
118
+ redirect(session.checkout_url);
119
+ ```
120
+
121
+ The browser must never choose the amount, currency, nonce, or return URL. The
122
+ session is single-use and expires.
123
+
124
+ ## 5. Fulfill from the signed webhook
125
+
126
+ The browser return path is not the source of truth. Use the signed webhook and
127
+ classify the confirmation:
128
+
129
+ ```ts
130
+ import {
131
+ classifyDirectPaymentConfirmation,
132
+ verifyDirectRequestPaymentWebhook,
133
+ } from "@siglume/direct-request-payment";
134
+
135
+ const { event } = await verifyDirectRequestPaymentWebhook(
136
+ process.env.SIGLUME_WEBHOOK_SECRET!,
137
+ rawRequestBody,
138
+ siglumeSignatureHeader,
139
+ );
140
+
141
+ if (event.type === "direct_payment.confirmed") {
142
+ const confirmation = classifyDirectPaymentConfirmation(event);
143
+
144
+ if (confirmation.kind === "standard_settled") {
145
+ await orders.markPaidOnceByChallengeHash(confirmation.challenge_hash, {
146
+ requirement_id: confirmation.requirement_id,
147
+ chain_receipt_id: confirmation.chain_receipt_id,
148
+ });
149
+ } else if (confirmation.kind === "metered_usage_accepted") {
150
+ await orders.markFulfilledButUnsettledOnceByChallengeHash(
151
+ confirmation.challenge_hash,
152
+ { requirement_id: confirmation.requirement_id },
153
+ );
154
+ } else {
155
+ await orders.flagForPaymentStateReview(confirmation);
156
+ }
157
+ }
158
+ ```
159
+
160
+ For this 10-minute guide, keep the test order in the Standard band so the
161
+ expected successful branch is `standard_settled`.
162
+
163
+ ## Done means
164
+
165
+ You are done with the quickstart when:
166
+
167
+ - the checkout session is created,
168
+ - the buyer reaches the Siglume wallet hosted checkout page,
169
+ - the signed webhook verifies against the raw body,
170
+ - `classifyDirectPaymentConfirmation(event)` returns `standard_settled`,
171
+ - your order is marked paid once, keyed by the stored `challenge_hash`.
172
+
173
+ Before production, complete the full checklist in
174
+ [Merchant Quickstart](./merchant-quickstart.md#go-live-checklist), read
175
+ [Payment lifecycle](./payment-lifecycle.md), and prepare the failure handling in
176
+ [Troubleshooting](./troubleshooting.md).
@@ -0,0 +1,62 @@
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
+ - The merchant account exists.
14
+ - The merchant billing mandate is active.
15
+ - The webhook callback URL is HTTPS and reachable.
16
+ - The checkout return URL origins are registered through
17
+ `checkout_allowed_origins`.
18
+ - The account has Hosted Checkout enabled.
19
+
20
+ If `createCheckoutSession(...)` or `getCheckoutSession(...)` raises
21
+ `HostedCheckoutNotAvailableError`, do not show the raw 404/409 to the buyer.
22
+ Stop the human checkout flow and contact Siglume support or your Siglume account
23
+ contact for Hosted Checkout enablement.
24
+
25
+ ## API errors
26
+
27
+ | Status / code | Likely cause | Retry? | Same idempotency key? | Buyer copy | Operator action |
28
+ | --- | --- | --- | --- | --- | --- |
29
+ | `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. |
30
+ | `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. |
31
+ | `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. |
32
+ | `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. |
33
+ | `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. |
34
+ | `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. |
35
+ | `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. |
36
+
37
+ ## Webhook failures
38
+
39
+ - Verify the exact raw request body bytes or raw body string.
40
+ - Do not verify a parsed JSON object or a re-stringified JSON body.
41
+ - Return a 2xx only after you have durably recorded the event or safely decided
42
+ it is duplicate/ignored.
43
+ - Store processed webhook event ids or settlement identifiers durably; an
44
+ in-memory set is not enough for production.
45
+ - Do not assume delivery order. A settlement batch event may be reconciled from
46
+ statement APIs rather than from one order challenge.
47
+ - On signature failure, return a non-2xx status and do not mutate order state.
48
+ - On a valid but unknown payment classification, return 2xx only after routing
49
+ it to durable manual review.
50
+
51
+ ## Refunds and adjustments
52
+
53
+ This SDK release does not expose a self-service refund API. For Standard
54
+ Payment refunds or Micro / Nano adjustments, use the explicit Siglume support or
55
+ platform process available to your account. Do not reverse settled revenue by
56
+ editing local statements or CSV exports.
57
+
58
+ ## Safe buyer messages
59
+
60
+ Keep buyer-facing messages short and non-diagnostic. Do not expose raw API
61
+ errors, wallet internals, RPC URLs, stack traces, webhook secrets, or support
62
+ 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.18,<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
+ }