@siglume/direct-request-payment 0.4.19 → 0.4.20
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 +25 -0
- package/README.md +12 -9
- package/bin/siglume-sdrp.mjs +135 -4
- package/dist/index.cjs +25 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -2
- package/dist/index.d.ts +26 -2
- package/dist/index.js +25 -1
- package/dist/index.js.map +1 -1
- package/docs/announcement-ja.md +17 -3
- package/docs/api-reference.md +57 -13
- package/docs/merchant-quickstart.md +6 -20
- package/docs/metered-statements.md +15 -13
- package/docs/payment-lifecycle.md +12 -9
- package/docs/pricing.md +7 -4
- package/docs/quickstart-10-minutes.md +46 -17
- package/docs/troubleshooting.md +15 -8
- package/examples/express-checkout.ts +37 -13
- package/examples/hosted-checkout-python/app.py +46 -31
- package/examples/hosted-checkout-python/order_store.py +13 -3
- package/examples/hosted-checkout-python/pyproject.toml +1 -1
- package/examples/hosted-checkout-typescript/src/order-store.ts +14 -3
- package/examples/hosted-checkout-typescript/src/server.ts +49 -37
- package/package.json +1 -1
- package/templates/express/README.md +24 -4
- package/templates/express/siglume-order-store.example.ts +22 -6
- package/templates/express/siglume-sdrp-routes.ts +138 -64
- package/templates/fastapi/README.md +5 -1
- package/templates/fastapi/siglume_order_store_example.py +29 -6
- package/templates/fastapi/siglume_sdrp_routes.py +112 -49
package/docs/api-reference.md
CHANGED
|
@@ -356,7 +356,8 @@ Returns:
|
|
|
356
356
|
`merchant.merchant_account.metadata_jsonb.metered_risk_acceptance` records the
|
|
357
357
|
merchant's Micro / Nano delayed-settlement risk acceptance receipt with
|
|
358
358
|
`terms_version`, `accepted_at`, `principal_user_id`, `receipt_id`, and fixed
|
|
359
|
-
market thresholds
|
|
359
|
+
market thresholds JPY 10,000 / USD 100.00 (`settlement_threshold_minor` is
|
|
360
|
+
`10000` for both JPY minor units and USD cents).
|
|
360
361
|
|
|
361
362
|
Secrets are returned only when created or rotated. Existing secrets are not
|
|
362
363
|
replayed by `getMerchant` / `get_merchant`.
|
|
@@ -372,7 +373,9 @@ POST /v1/sdrp/direct-payments/merchants
|
|
|
372
373
|
Creates or updates the merchant account for the authenticated merchant user.
|
|
373
374
|
Accepts the optional `checkout_allowed_origins: string[]` return-URL origin
|
|
374
375
|
allowlist described under `setupCheckout` above; the same normalization and
|
|
375
|
-
webhook-origin auto-allow apply.
|
|
376
|
+
webhook-origin auto-allow apply. Python annotates this direct response as
|
|
377
|
+
`DirectRequestPaymentMerchantResponse`; `setup_checkout(...)` returns
|
|
378
|
+
`DirectRequestPaymentCheckoutSetupResult`.
|
|
376
379
|
|
|
377
380
|
### `createCheckoutSession(input)` / `create_checkout_session(...)`
|
|
378
381
|
|
|
@@ -535,6 +538,41 @@ Defaults event types to `direct_payment.confirmed` and
|
|
|
535
538
|
`direct_payment.spent`. The returned `signing_secret` is shown only at creation
|
|
536
539
|
or rotation.
|
|
537
540
|
|
|
541
|
+
### `listWebhookSubscriptions()` / `list_webhook_subscriptions()`
|
|
542
|
+
|
|
543
|
+
Calls:
|
|
544
|
+
|
|
545
|
+
```text
|
|
546
|
+
GET /v1/market/webhooks/subscriptions
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Returns the current user's webhook subscriptions without the full signing
|
|
550
|
+
secret. Use `signing_secret_hint` to confirm that the local
|
|
551
|
+
`SIGLUME_WEBHOOK_SECRET` is the expected secret.
|
|
552
|
+
|
|
553
|
+
### `queueWebhookTestDelivery(input)` / `queue_webhook_test_delivery(...)`
|
|
554
|
+
|
|
555
|
+
Calls:
|
|
556
|
+
|
|
557
|
+
```text
|
|
558
|
+
POST /v1/market/webhooks/test-deliveries
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
Queues a signed test event to one or more subscription ids. `siglume-check
|
|
562
|
+
readiness` uses this for a harmless `direct_payment.confirmed` readiness probe.
|
|
563
|
+
|
|
564
|
+
### `listWebhookDeliveries(input)` / `list_webhook_deliveries(...)`
|
|
565
|
+
|
|
566
|
+
Calls:
|
|
567
|
+
|
|
568
|
+
```text
|
|
569
|
+
GET /v1/market/webhooks/deliveries
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
Supports `subscription_id`, `event_type`, `status`, and `limit`. Readiness polls
|
|
573
|
+
this list after queueing the test delivery and only passes when the matching
|
|
574
|
+
delivery status becomes `delivered`.
|
|
575
|
+
|
|
538
576
|
## `DirectRequestPaymentClient`
|
|
539
577
|
|
|
540
578
|
Thin wrapper around the current Siglume Direct Request Payment HTTP contract.
|
|
@@ -1012,10 +1050,10 @@ the `Siglume-Signature` header; use
|
|
|
1012
1050
|
`verifyDirectRequestPaymentWebhook(...)` /
|
|
1013
1051
|
`verify_direct_request_payment_webhook(...)` for signature verification and
|
|
1014
1052
|
parsing together. Throws
|
|
1015
|
-
`SiglumeWebhookPayloadError` on a malformed event
|
|
1016
|
-
`direct_payment.confirmed`
|
|
1017
|
-
|
|
1018
|
-
argument is positional in both languages.
|
|
1053
|
+
`SiglumeWebhookPayloadError` on a malformed event. It does not reject an
|
|
1054
|
+
unsupported `direct_payment.confirmed` mode by itself; the classifier returns
|
|
1055
|
+
`kind: "unknown"` with `reason: "unsupported_confirmation_mode"` for that case.
|
|
1056
|
+
The `payload` argument is positional in both languages.
|
|
1019
1057
|
|
|
1020
1058
|
For `direct_payment.confirmed`, inspect `event.data.pricing_band`,
|
|
1021
1059
|
`event.data.settlement_cadence`, `event.data.finality`,
|
|
@@ -1039,13 +1077,15 @@ Recommended branch: call `classifyDirectPaymentConfirmation(event)` /
|
|
|
1039
1077
|
pricing, per-payment on-chain finality, settled status, non-empty
|
|
1040
1078
|
`requirement_id`, non-empty `challenge_hash`, and non-empty
|
|
1041
1079
|
`chain_receipt_id`.
|
|
1042
|
-
- `metered_usage_accepted`: treat the usage as accepted but unsettled
|
|
1043
|
-
|
|
1044
|
-
|
|
1080
|
+
- `metered_usage_accepted`: treat the usage as accepted but unsettled only if
|
|
1081
|
+
your integration has explicitly enabled Micro / Nano delayed-settlement
|
|
1082
|
+
handling. This requires Micro / Nano pricing,
|
|
1083
|
+
`finality === "aggregated_onchain_settlement"`, the matching settlement
|
|
1084
|
+
cadence (`micro` -> `weekly`, `nano` -> `monthly`),
|
|
1045
1085
|
`settlement_status === "pending_settlement"`, non-empty `requirement_id`, and
|
|
1046
|
-
non-empty `challenge_hash`.
|
|
1047
|
-
|
|
1048
|
-
|
|
1086
|
+
non-empty `challenge_hash`. Standard-only integrations should route this to
|
|
1087
|
+
review or return `METERED_INTEGRATION_REQUIRED`; Micro / Nano integrations
|
|
1088
|
+
must reconcile final revenue from statement APIs / settlement batches.
|
|
1049
1089
|
- `unknown`: do not mark paid or fulfilled from the event type alone; fetch the
|
|
1050
1090
|
requirement or route the event to manual review.
|
|
1051
1091
|
|
|
@@ -1153,7 +1193,11 @@ package exports `TypedDict` names for the high-risk response shapes:
|
|
|
1153
1193
|
- `DirectRequestPaymentPastDueBlock`
|
|
1154
1194
|
- `DirectRequestPaymentProviderMeteredTotals`
|
|
1155
1195
|
- `DirectRequestPaymentListResponse`
|
|
1156
|
-
- `
|
|
1196
|
+
- `DirectRequestPaymentMerchantResponse`
|
|
1197
|
+
- `DirectRequestPaymentCheckoutSetupResult`
|
|
1198
|
+
- `DirectRequestPaymentMerchantSetupResponse` (compatibility alias for checkout setup result)
|
|
1199
|
+
- `DirectRequestPaymentWebhookSubscription`
|
|
1200
|
+
- `DirectRequestPaymentWebhookDelivery`
|
|
1157
1201
|
- `DirectRequestPaymentWebhookVerification`
|
|
1158
1202
|
- `DirectRequestPaymentConfirmationClassification`
|
|
1159
1203
|
|
|
@@ -459,16 +459,9 @@ if (confirmation.kind === "standard_settled") {
|
|
|
459
459
|
}
|
|
460
460
|
|
|
461
461
|
if (confirmation.kind === "metered_usage_accepted") {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
reason: "unknown_metered_challenge_hash",
|
|
466
|
-
requirement_id: confirmation.requirement_id,
|
|
467
|
-
});
|
|
468
|
-
return new Response(null, { status: 204 });
|
|
469
|
-
}
|
|
470
|
-
await orders.markFulfilledButUnsettledOnce(order.id, {
|
|
471
|
-
siglume_requirement_id: confirmation.requirement_id,
|
|
462
|
+
await orders.flagForPaymentStateReview({
|
|
463
|
+
reason: "metered_integration_required",
|
|
464
|
+
requirement_id: confirmation.requirement_id,
|
|
472
465
|
pricing_band: confirmation.pricing_band,
|
|
473
466
|
});
|
|
474
467
|
return new Response(null, { status: 204 });
|
|
@@ -531,16 +524,9 @@ if confirmation["kind"] == "standard_settled":
|
|
|
531
524
|
return "", 204
|
|
532
525
|
|
|
533
526
|
if confirmation["kind"] == "metered_usage_accepted":
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
reason="unknown_metered_challenge_hash",
|
|
538
|
-
requirement_id=confirmation["requirement_id"],
|
|
539
|
-
)
|
|
540
|
-
return "", 204
|
|
541
|
-
orders.mark_fulfilled_but_unsettled_once(
|
|
542
|
-
order["id"],
|
|
543
|
-
siglume_requirement_id=confirmation["requirement_id"],
|
|
527
|
+
orders.flag_for_payment_state_review(
|
|
528
|
+
reason="metered_integration_required",
|
|
529
|
+
requirement_id=confirmation["requirement_id"],
|
|
544
530
|
pricing_band=confirmation["pricing_band"],
|
|
545
531
|
)
|
|
546
532
|
return "", 204
|
|
@@ -195,9 +195,12 @@ Threshold-control fields:
|
|
|
195
195
|
- `settlement_trigger`: `amount_threshold` or `scheduled_close`
|
|
196
196
|
- `settlement_threshold_minor`: JPY `10000` or USD `10000` minor units
|
|
197
197
|
- `threshold_reached_at`: set when the fixed amount threshold closed the batch
|
|
198
|
-
- `total_unsettled_exposure_minor`:
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
- `total_unsettled_exposure_minor`: chargeable provider gross exposure for the
|
|
199
|
+
same buyer / provider / token / pricing band where the batch is not
|
|
200
|
+
`settled`, `uncollectible`, or `written_off`. This includes open usage,
|
|
201
|
+
`notice_pending`, `notice_delivery_failed`, `ready`, `submitted`,
|
|
202
|
+
`submitted_reconcile_required`, `failed_retryable`, `retrying`, and
|
|
203
|
+
`past_due`.
|
|
201
204
|
|
|
202
205
|
JPY 10,000 and USD 100.00 are market-specific fixed thresholds, not FX
|
|
203
206
|
conversions of one another.
|
|
@@ -318,7 +321,7 @@ Important batch fields:
|
|
|
318
321
|
| `settlement_trigger` | `amount_threshold` for early threshold close, or `scheduled_close` for weekly/monthly close |
|
|
319
322
|
| `settlement_threshold_minor` | Fixed market threshold for early settlement: JPY `10000` or USD `10000` minor units |
|
|
320
323
|
| `threshold_reached_at` | Timestamp when the fixed threshold closed the batch, otherwise null |
|
|
321
|
-
| `total_unsettled_exposure_minor` |
|
|
324
|
+
| `total_unsettled_exposure_minor` | Chargeable provider gross exposure for the same buyer / provider / token / pricing band where status is not `settled`, `uncollectible`, or `written_off`; includes open, notice, ready, submitted, reconcile-required, retryable, retrying, and past-due states |
|
|
322
325
|
| `expected_scheduled_debit_at` | Expected debit time for an open period before a batch exists |
|
|
323
326
|
| `scheduled_debit_at` | Scheduled debit time after batch creation |
|
|
324
327
|
| `not_before_attempt_at` | Earliest allowed debit attempt; this is the close-plus-3-day gate |
|
|
@@ -398,10 +401,10 @@ Siglume retries failed Micro / Nano settlement every 6 hours for up to 28
|
|
|
398
401
|
automatic attempts. After that the batch remains `past_due` until operator
|
|
399
402
|
requeue.
|
|
400
403
|
|
|
401
|
-
New Micro / Nano usage for the same buyer / provider / token
|
|
402
|
-
the total unsettled exposure is at or above the fixed threshold,
|
|
403
|
-
failed or past-due block remains. The provider API is not called for
|
|
404
|
-
rejected request, and the request is not charged.
|
|
404
|
+
New Micro / Nano usage for the same buyer / provider / token / pricing band is
|
|
405
|
+
paused while the total unsettled exposure is at or above the fixed threshold,
|
|
406
|
+
and while a failed or past-due block remains. The provider API is not called for
|
|
407
|
+
the rejected request, and the request is not charged.
|
|
405
408
|
|
|
406
409
|
Public failure fields are sanitized. Show `failure_reason_code`,
|
|
407
410
|
`failure_reason_label`, `failure_reason_help`, and `support_reference` to users
|
|
@@ -449,13 +452,12 @@ using stable idempotency keys and provider-side completion records.
|
|
|
449
452
|
| --- | --- | --- | --- | --- |
|
|
450
453
|
| `notice_delivery_failed` | Buyer debit is not yet allowed; provider revenue remains unsettled | Notice delivery can be retried or reviewed | Required if delivery keeps failing | Do not attempt your own debit notice or mark revenue settled. Show support context only. |
|
|
451
454
|
| `submitted_reconcile_required` | A settlement submission exists but final on-chain outcome is not yet reconciled | Reconciliation may complete if a receipt is found | Required if reconciliation stalls | Do not retry payment yourself. Wait for `settled`, `failed_retryable`, or `past_due`. |
|
|
452
|
-
| `past_due` | Buyer has an unresolved settlement block; provider sees past-due revenue | New Micro / Nano usage for the same buyer /
|
|
455
|
+
| `past_due` | Buyer has an unresolved settlement block; provider sees past-due revenue | New Micro / Nano usage for the same buyer / provider / token / pricing band is paused | Operator requeue or manual resolution only | Do not promise collection or provider payment. Ask the buyer to repair balance / allowance / BudgetVault / caps and reference `support_reference`. |
|
|
453
456
|
| `failed_chargeable` | Usage is still chargeable because provider work was accepted or completed | Included in later settlement attempts | Review if the provider disputes completion | Keep fulfillment idempotent and preserve evidence keyed by idempotency key. |
|
|
454
457
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
`chain_receipt_id` is present.
|
|
458
|
+
Terminal public states include `uncollectible` and `written_off` after operator
|
|
459
|
+
review. Treat unknown terminal settlement states as not settled unless
|
|
460
|
+
`status === "settled"` and `chain_receipt_id` is present.
|
|
459
461
|
|
|
460
462
|
## Operational Recipes
|
|
461
463
|
|
|
@@ -32,7 +32,7 @@ checkout open or agent/API payment starts
|
|
|
32
32
|
-> usage accepted
|
|
33
33
|
-> direct_payment.confirmed webhook
|
|
34
34
|
-> classifier kind: metered_usage_accepted
|
|
35
|
-
-> merchant may fulfill as fulfilled_unsettled
|
|
35
|
+
-> merchant may fulfill as fulfilled_unsettled only after enabling Micro/Nano handling
|
|
36
36
|
-> open period closes by amount threshold or schedule
|
|
37
37
|
-> final notice window
|
|
38
38
|
-> submitted / retrying / past_due if needed
|
|
@@ -41,10 +41,12 @@ checkout open or agent/API payment starts
|
|
|
41
41
|
-> provider revenue is settled
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
For Micro / Nano, `metered_usage_accepted` means the usage can be fulfilled
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
For Micro / Nano, `metered_usage_accepted` means the usage can be fulfilled only
|
|
45
|
+
by an integration that has explicitly accepted SDRP delayed settlement and
|
|
46
|
+
implemented fulfilled-but-unsettled state, settlement reconciliation, past-due
|
|
47
|
+
handling, and terminal accounting. Provider revenue is not settled yet. Provider
|
|
48
|
+
revenue becomes settled only when the settlement batch is settled on-chain and
|
|
49
|
+
has a `chain_receipt_id`.
|
|
48
50
|
|
|
49
51
|
## Field meanings
|
|
50
52
|
|
|
@@ -52,7 +54,7 @@ on-chain and has a `chain_receipt_id`.
|
|
|
52
54
|
| --- | --- | --- |
|
|
53
55
|
| Hosted Checkout `status: "paid"` | The checkout session accepted the wallet payment flow. | For Micro / Nano, it does not mean provider revenue is settled. |
|
|
54
56
|
| `standard_settled` | Standard payment is on-chain settled and can mark an order paid. | It is not used for Micro / Nano accepted usage. |
|
|
55
|
-
| `metered_usage_accepted` | Micro / Nano usage is accepted
|
|
57
|
+
| `metered_usage_accepted` | Micro / Nano usage is accepted for integrations that have enabled delayed settlement handling. | It is not settled provider revenue, and Standard-only integrations should not fulfill it. |
|
|
56
58
|
| `fulfilled_unsettled` | Your merchant system delivered the item before Micro / Nano settlement. | It is not a Siglume settlement status. |
|
|
57
59
|
| `metered_batch_settled` | Aggregated Micro / Nano batch settled on-chain. | It does not identify one order by challenge hash. |
|
|
58
60
|
| `pending_settlement` | Micro / Nano usage is waiting for aggregated settlement. | It is not a failure by itself. |
|
|
@@ -65,9 +67,10 @@ on-chain and has a `chain_receipt_id`.
|
|
|
65
67
|
re-stringified JSON object.
|
|
66
68
|
- Store `challenge_hash` on the order before redirecting the buyer.
|
|
67
69
|
- For Standard, mark paid only from `standard_settled`.
|
|
68
|
-
- For Micro / Nano,
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
- For Micro / Nano, fulfill only after you have explicitly enabled delayed
|
|
71
|
+
settlement handling. Use a separate local state such as
|
|
72
|
+
`fulfilled_unsettled`, then reconcile final revenue from statement APIs and
|
|
73
|
+
batch settlement events.
|
|
71
74
|
- Treat `unknown` classifications as manual review. Do not mark paid or
|
|
72
75
|
fulfilled from the event name alone.
|
|
73
76
|
|
package/docs/pricing.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Pricing
|
|
2
2
|
|
|
3
3
|
This page documents the trial-phase merchant pricing for Siglume Direct Request
|
|
4
|
-
Payment as of SDK v0.4.
|
|
4
|
+
Payment as of SDK v0.4.20. Pricing can change by agreement or future product
|
|
5
5
|
release; the Siglume platform response is the source of truth for per-payment
|
|
6
6
|
fee data returned at runtime.
|
|
7
7
|
|
|
@@ -156,9 +156,12 @@ confirmed payment turns into money in your settlement wallet.
|
|
|
156
156
|
- While the same buyer / provider / token / pricing band has total unsettled
|
|
157
157
|
exposure at or above the fixed threshold, new Micro/Nano usage is paused with the
|
|
158
158
|
machine-readable error `METERED_SETTLEMENT_PAST_DUE`; the provider API is not
|
|
159
|
-
called. Exposure
|
|
160
|
-
`
|
|
161
|
-
|
|
159
|
+
called. Exposure is chargeable provider gross where status is not `settled`,
|
|
160
|
+
`uncollectible`, or `written_off`; it includes open usage,
|
|
161
|
+
`notice_pending`, `notice_delivery_failed`, `ready`, `submitted`,
|
|
162
|
+
`submitted_reconcile_required`, `failed_retryable`, `retrying`, and
|
|
163
|
+
`past_due`. Usage remains paused while settlement failure or `past_due` is
|
|
164
|
+
unresolved.
|
|
162
165
|
- Outstanding amounts remain attached to the failed settlement and are retried
|
|
163
166
|
under this policy. They are not settled revenue, and Siglume does not advance,
|
|
164
167
|
guarantee, or insure provider revenue before on-chain settlement succeeds.
|
|
@@ -34,27 +34,35 @@ SIGLUME_MERCHANT_AUTH_TOKEN=<merchant Siglume bearer token>
|
|
|
34
34
|
SIGLUME_DIRECT_PAYMENT_MERCHANT=<merchant key>
|
|
35
35
|
SHOP_PUBLIC_ORIGIN=https://www.your-product.example
|
|
36
36
|
SHOP_WEBHOOK_URL=https://api.your-product.example/payments/webhooks/siglume
|
|
37
|
+
SIGLUME_WEBHOOK_SECRET=<webhook signing secret from setupCheckout/setup_checkout>
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
Then run:
|
|
40
|
+
Then run the matching CLI:
|
|
40
41
|
|
|
41
42
|
```bash
|
|
43
|
+
# Node / Express
|
|
42
44
|
npx siglume-check readiness
|
|
45
|
+
|
|
46
|
+
# Python / FastAPI
|
|
47
|
+
siglume-check readiness
|
|
43
48
|
```
|
|
44
49
|
|
|
45
50
|
The readiness check fails before you write checkout code if any required item is
|
|
46
|
-
missing. It checks local config, reads the merchant account,
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
missing. It checks local config, reads the merchant account, requires active
|
|
52
|
+
billing, confirms the webhook subscription points to `SHOP_WEBHOOK_URL`, checks
|
|
53
|
+
that `direct_payment.confirmed` is subscribed, verifies the local webhook secret
|
|
54
|
+
against the subscription hint, creates one unpaid expiring Hosted Checkout probe
|
|
55
|
+
session, and queues a signed webhook test delivery. No buyer is charged.
|
|
49
56
|
|
|
50
|
-
For CI
|
|
57
|
+
For a CI local-config smoke test:
|
|
51
58
|
|
|
52
59
|
```bash
|
|
53
|
-
npx siglume-check readiness --json
|
|
60
|
+
npx siglume-check readiness --no-api --json
|
|
54
61
|
```
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
checkout path
|
|
63
|
+
`--no-api` does not prove Hosted Checkout or webhook delivery. Before opening a
|
|
64
|
+
human web checkout path, run readiness without `--no-api` and fix every FAIL
|
|
65
|
+
item.
|
|
58
66
|
|
|
59
67
|
## 1. Copy integration files into your product
|
|
60
68
|
|
|
@@ -78,16 +86,31 @@ generated files are intentionally small and are meant to be edited.
|
|
|
78
86
|
Express:
|
|
79
87
|
|
|
80
88
|
```ts
|
|
81
|
-
import
|
|
89
|
+
import express from "express";
|
|
90
|
+
import {
|
|
91
|
+
createSiglumeSdrpCheckoutRouter,
|
|
92
|
+
createSiglumeSdrpWebhookHandler,
|
|
93
|
+
type SiglumeSdrpRouterOptions,
|
|
94
|
+
} from "./siglume/siglume-sdrp-routes.js";
|
|
82
95
|
import { siglumeOrderStore } from "./siglume/siglume-order-store.example.js";
|
|
83
96
|
|
|
84
|
-
|
|
97
|
+
const siglumeOptions: SiglumeSdrpRouterOptions = {
|
|
85
98
|
merchant: process.env.SIGLUME_DIRECT_PAYMENT_MERCHANT!,
|
|
86
99
|
merchant_auth_token: process.env.SIGLUME_MERCHANT_AUTH_TOKEN!,
|
|
87
100
|
webhook_secret: process.env.SIGLUME_WEBHOOK_SECRET!,
|
|
88
101
|
shop_public_origin: process.env.SHOP_PUBLIC_ORIGIN!,
|
|
89
102
|
order_store: siglumeOrderStore,
|
|
90
|
-
|
|
103
|
+
allow_metered_payments: false,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
app.post(
|
|
107
|
+
"/payments/webhooks/siglume",
|
|
108
|
+
express.raw({ type: "application/json" }),
|
|
109
|
+
createSiglumeSdrpWebhookHandler(siglumeOptions),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
app.use(express.json());
|
|
113
|
+
app.use("/payments", createSiglumeSdrpCheckoutRouter(siglumeOptions));
|
|
91
114
|
```
|
|
92
115
|
|
|
93
116
|
FastAPI:
|
|
@@ -97,7 +120,7 @@ from .siglume.siglume_order_store_example import ExampleSiglumeOrderStore
|
|
|
97
120
|
from .siglume.siglume_sdrp_routes import create_siglume_sdrp_router
|
|
98
121
|
|
|
99
122
|
app.include_router(
|
|
100
|
-
create_siglume_sdrp_router(ExampleSiglumeOrderStore()),
|
|
123
|
+
create_siglume_sdrp_router(ExampleSiglumeOrderStore(), allow_metered_payments=False),
|
|
101
124
|
prefix="/payments",
|
|
102
125
|
)
|
|
103
126
|
```
|
|
@@ -109,14 +132,20 @@ Replace the example store with your product's order database. The adapter must:
|
|
|
109
132
|
- load the order by your `order_id`,
|
|
110
133
|
- verify the current user is allowed to pay for that order,
|
|
111
134
|
- return the server-authored `amount_minor` and `currency`,
|
|
112
|
-
-
|
|
113
|
-
-
|
|
135
|
+
- create or reuse one active checkout attempt with a stable nonce,
|
|
136
|
+
- persist `challenge_hash`, `checkout_session_id`, and `checkout_url` before redirecting,
|
|
137
|
+
- process webhook event ids durably in the same transaction as the order update,
|
|
114
138
|
- mark Standard orders paid exactly once,
|
|
115
|
-
- mark Micro / Nano orders as fulfilled but unsettled exactly once,
|
|
116
139
|
- route unknown classifications to manual review.
|
|
117
140
|
|
|
118
141
|
Do not calculate the amount from browser input.
|
|
119
142
|
|
|
143
|
+
The generated route defaults to Standard-only. If an order amount falls into
|
|
144
|
+
Micro / Nano, checkout returns `METERED_INTEGRATION_REQUIRED` until you set
|
|
145
|
+
`allow_metered_payments: true` / `allow_metered_payments=True` and implement
|
|
146
|
+
fulfilled-but-unsettled state, settlement reconciliation, past-due handling, and
|
|
147
|
+
terminal write-off handling.
|
|
148
|
+
|
|
120
149
|
## 4. Start checkout from your frontend
|
|
121
150
|
|
|
122
151
|
Call your own server route:
|
|
@@ -135,10 +164,10 @@ Your product is integrated when:
|
|
|
135
164
|
|
|
136
165
|
- `npx siglume-check readiness` passes,
|
|
137
166
|
- your product has mounted checkout and webhook routes,
|
|
138
|
-
- your order database stores `challenge_hash` for the order,
|
|
167
|
+
- your order database stores one active checkout attempt and `challenge_hash` for the order,
|
|
139
168
|
- the signed webhook verifies against the raw body,
|
|
140
169
|
- `standard_settled` marks the order paid once,
|
|
141
|
-
-
|
|
170
|
+
- duplicate webhook deliveries do not double-fulfill the order.
|
|
142
171
|
|
|
143
172
|
For Micro / Nano revenue reconciliation, read
|
|
144
173
|
[Payment lifecycle](./payment-lifecycle.md) and
|
package/docs/troubleshooting.md
CHANGED
|
@@ -14,16 +14,22 @@ building a human web checkout:
|
|
|
14
14
|
npx siglume-check readiness
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
The command validates local configuration, reads the merchant account,
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
The command validates local configuration, reads the merchant account, checks
|
|
18
|
+
the active billing mandate, confirms the webhook subscription, creates one
|
|
19
|
+
unpaid expiring checkout session, and queues a signed webhook test delivery.
|
|
20
20
|
|
|
21
21
|
- The merchant account exists.
|
|
22
22
|
- The merchant billing mandate is active.
|
|
23
|
-
-
|
|
23
|
+
- `SIGLUME_WEBHOOK_SECRET` is present and matches the subscription secret hint.
|
|
24
|
+
- The webhook callback URL is HTTPS and matches an active subscription.
|
|
25
|
+
- The subscription includes `direct_payment.confirmed`.
|
|
24
26
|
- The checkout return URL origins are registered through
|
|
25
27
|
`checkout_allowed_origins`.
|
|
26
28
|
- The account has Hosted Checkout enabled.
|
|
29
|
+
- The signed webhook test delivery reaches the endpoint and returns success.
|
|
30
|
+
|
|
31
|
+
`--no-api` is only for local config smoke tests. `--no-probe` is a partial API
|
|
32
|
+
check and does not report readiness as ready.
|
|
27
33
|
|
|
28
34
|
If `createCheckoutSession(...)` or `getCheckoutSession(...)` raises
|
|
29
35
|
`HostedCheckoutNotAvailableError`, do not show the raw 404/409 to the buyer.
|
|
@@ -46,10 +52,11 @@ contact for Hosted Checkout enablement.
|
|
|
46
52
|
|
|
47
53
|
- Verify the exact raw request body bytes or raw body string.
|
|
48
54
|
- Do not verify a parsed JSON object or a re-stringified JSON body.
|
|
49
|
-
- Return a 2xx only after
|
|
50
|
-
|
|
51
|
-
- Store processed webhook event ids or settlement identifiers durably
|
|
52
|
-
|
|
55
|
+
- Return a 2xx only after the order update or durable manual-review write has
|
|
56
|
+
succeeded, or after you safely decided the event is duplicate/ignored.
|
|
57
|
+
- Store processed webhook event ids or settlement identifiers durably, in the
|
|
58
|
+
same database transaction as the order update/review write. An in-memory set
|
|
59
|
+
is not enough for production.
|
|
53
60
|
- Do not assume delivery order. A settlement batch event may be reconciled from
|
|
54
61
|
statement APIs rather than from one order challenge.
|
|
55
62
|
- On signature failure, return a non-2xx status and do not mutate order state.
|
|
@@ -36,11 +36,21 @@ app.use((req, res, next) => {
|
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
const orders = new Map<string, any>();
|
|
39
|
+
const processedWebhookEvents = new Set<string>();
|
|
39
40
|
|
|
40
41
|
async function flagForPaymentStateReview(payload: Record<string, any>): Promise<void> {
|
|
41
42
|
console.warn("payment state review required", payload);
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
async function processWebhookEventOnce(eventId: string, handler: () => Promise<void>): Promise<"processed" | "duplicate"> {
|
|
46
|
+
if (processedWebhookEvents.has(eventId)) {
|
|
47
|
+
return "duplicate";
|
|
48
|
+
}
|
|
49
|
+
await handler();
|
|
50
|
+
processedWebhookEvents.add(eventId);
|
|
51
|
+
return "processed";
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
async function handleDirectPaymentConfirmed(event: any): Promise<void> {
|
|
45
55
|
const classification = classifyDirectPaymentConfirmation(event);
|
|
46
56
|
|
|
@@ -67,16 +77,11 @@ async function handleDirectPaymentConfirmed(event: any): Promise<void> {
|
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
if (classification.kind === "metered_usage_accepted") {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
await flagForPaymentStateReview({
|
|
76
|
-
reason: "unknown_metered_challenge_hash",
|
|
77
|
-
requirement_id: classification.requirement_id,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
+
await flagForPaymentStateReview({
|
|
81
|
+
reason: "metered_integration_required",
|
|
82
|
+
requirement_id: classification.requirement_id,
|
|
83
|
+
pricing_band: classification.pricing_band,
|
|
84
|
+
});
|
|
80
85
|
return;
|
|
81
86
|
}
|
|
82
87
|
|
|
@@ -102,7 +107,19 @@ app.post("/checkout/siglume/start", asyncRoute(async (req, res) => {
|
|
|
102
107
|
return;
|
|
103
108
|
}
|
|
104
109
|
|
|
105
|
-
|
|
110
|
+
if (!Number(order.payment_attempt || 0)) {
|
|
111
|
+
order.payment_attempt = 1;
|
|
112
|
+
}
|
|
113
|
+
if (order.siglume_checkout_url && order.siglume_checkout_session_id) {
|
|
114
|
+
res.json({
|
|
115
|
+
order_id: order.id,
|
|
116
|
+
amount_minor: order.amount_minor,
|
|
117
|
+
currency: order.currency,
|
|
118
|
+
checkout_url: order.siglume_checkout_url,
|
|
119
|
+
session_id: order.siglume_checkout_session_id,
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
106
123
|
const session = await siglumeMerchant.createCheckoutSession({
|
|
107
124
|
merchant: merchantKey,
|
|
108
125
|
amount_minor: order.amount_minor,
|
|
@@ -114,6 +131,7 @@ app.post("/checkout/siglume/start", asyncRoute(async (req, res) => {
|
|
|
114
131
|
});
|
|
115
132
|
|
|
116
133
|
order.siglume_challenge_hash = session.challenge_hash;
|
|
134
|
+
order.siglume_checkout_url = session.checkout_url;
|
|
117
135
|
order.siglume_checkout_session_id = session.session_id;
|
|
118
136
|
order.siglume_payment_status = "pending";
|
|
119
137
|
|
|
@@ -134,8 +152,14 @@ app.post("/siglume/webhook", express.raw({ type: "application/json" }), asyncRou
|
|
|
134
152
|
header,
|
|
135
153
|
);
|
|
136
154
|
|
|
137
|
-
|
|
138
|
-
|
|
155
|
+
const result = await processWebhookEventOnce(event.id, async () => {
|
|
156
|
+
if (event.type === "direct_payment.confirmed") {
|
|
157
|
+
await handleDirectPaymentConfirmed(event);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
if (result === "duplicate") {
|
|
161
|
+
res.status(204).send();
|
|
162
|
+
return;
|
|
139
163
|
}
|
|
140
164
|
|
|
141
165
|
res.status(204).send();
|