@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.
- package/CHANGELOG.md +21 -0
- package/README.md +46 -4
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/docs/announcement-ja.md +5 -3
- package/docs/api-reference.md +34 -1
- package/docs/merchant-quickstart.md +20 -2
- package/docs/payment-lifecycle.md +85 -0
- package/docs/pricing.md +6 -6
- package/docs/quickstart-10-minutes.md +176 -0
- package/docs/troubleshooting.md +62 -0
- package/examples/express-checkout.ts +14 -0
- package/examples/hosted-checkout-python/.env.example +5 -0
- package/examples/hosted-checkout-python/README.md +21 -0
- package/examples/hosted-checkout-python/app.py +124 -0
- package/examples/hosted-checkout-python/order_store.py +42 -0
- package/examples/hosted-checkout-python/pyproject.toml +9 -0
- package/examples/hosted-checkout-typescript/.env.example +5 -0
- package/examples/hosted-checkout-typescript/README.md +21 -0
- package/examples/hosted-checkout-typescript/package.json +20 -0
- package/examples/hosted-checkout-typescript/src/order-store.ts +52 -0
- package/examples/hosted-checkout-typescript/src/server.ts +139 -0
- package/examples/hosted-checkout-typescript/tsconfig.json +13 -0
- package/package.json +4 -1
|
@@ -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
|
|
48
|
-
|
|
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.
|
|
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,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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,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,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
|
+
}
|