@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.
- package/CHANGELOG.md +40 -0
- package/README.md +60 -4
- package/bin/siglume-sdrp.mjs +267 -0
- 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 +19 -2
- package/docs/payment-lifecycle.md +85 -0
- package/docs/pricing.md +6 -6
- package/docs/quickstart-10-minutes.md +145 -0
- package/docs/troubleshooting.md +70 -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 +12 -1
- package/templates/express/README.md +22 -0
- package/templates/express/siglume-order-store.example.ts +53 -0
- package/templates/express/siglume-sdrp-routes.ts +157 -0
- package/templates/fastapi/README.md +22 -0
- package/templates/fastapi/siglume_order_store_example.py +54 -0
- 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
|
|
48
|
-
|
|
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.
|
|
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,
|
|
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,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,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
|
+
}
|