@siglume/direct-request-payment 0.4.6 → 0.4.8

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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.8 - 2026-06-19
4
+
5
+ Webhook sample hardening release.
6
+
7
+ - Added TypeScript and Python confirmation classifiers for Standard settled
8
+ payments, Micro / Nano accepted usage, and Micro / Nano settled batches.
9
+ - Updated public webhook samples to require finality, settlement status, and
10
+ non-empty settlement identifiers before fulfilling or reconciling.
11
+ - Routed unknown Micro / Nano challenge hashes and malformed settlement batch
12
+ confirmations to manual review in copy-paste samples.
13
+
14
+ ## 0.4.7 - 2026-06-19
15
+
16
+ Documentation-only patch release.
17
+
18
+ - Clarified that SDRP merchant setup and billing mandate terms assume acceptance
19
+ of automatic Micro / Nano delayed aggregated settlement for low-price bands.
20
+ - Replaced merchant-specific override wording with the operational rule that
21
+ products requiring immediate settlement should be priced in the Standard band.
22
+
3
23
  ## 0.4.6 - 2026-06-19
4
24
 
5
25
  Patch release for the external webhook-state re-review.
package/README.md CHANGED
@@ -103,10 +103,10 @@ const session = await merchant.createCheckoutSession({
103
103
  });
104
104
  redirect(session.checkout_url); // -> https://siglume.com/pay/<session_id>
105
105
 
106
- // 3. Handle the signed direct_payment.confirmed webhook. Fulfill Standard only
107
- // when pricing_band=standard, finality=per_payment_onchain, and
108
- // settlement_status=settled. Treat Micro / Nano as accepted but unsettled
109
- // until the later metered settlement batch is settled.
106
+ // 3. Handle the signed direct_payment.confirmed webhook. Use
107
+ // classifyDirectPaymentConfirmation(event). Fulfill Standard only for
108
+ // standard_settled; treat metered_usage_accepted as fulfilled-unsettled
109
+ // until the later metered_batch_settled event arrives.
110
110
  // Poll merchant.getCheckoutSession(session.session_id) if you also want to
111
111
  // show status in your own UI.
112
112
  ```
@@ -138,10 +138,10 @@ session = merchant.create_checkout_session(
138
138
  )
139
139
  redirect(session["checkout_url"]) # -> https://siglume.com/pay/<session_id>
140
140
 
141
- # 3. Handle the signed direct_payment.confirmed webhook. Fulfill Standard only
142
- # when pricing_band=standard, finality=per_payment_onchain, and
143
- # settlement_status=settled. Treat Micro / Nano as accepted but unsettled
144
- # until the later metered settlement batch is settled.
141
+ # 3. Handle the signed direct_payment.confirmed webhook. Use
142
+ # classify_direct_payment_confirmation(event). Fulfill Standard only for
143
+ # standard_settled; treat metered_usage_accepted as fulfilled-unsettled
144
+ # until the later metered_batch_settled event arrives.
145
145
  # Poll merchant.get_checkout_session(session["session_id"]) if you also want
146
146
  # to show status in your own UI.
147
147
  ```
@@ -193,10 +193,12 @@ weekly / monthly settlement schedule are in [docs/pricing.md](./docs/pricing.md)
193
193
  Provider revenue in the Micro and Nano bands is not settled revenue until the
194
194
  weekly or monthly on-chain settlement succeeds. Siglume keeps outstanding failed
195
195
  settlements for retry under the published policy, but does not advance or
196
- guarantee provider revenue before settlement succeeds.
197
- If your product cannot fulfill before provider revenue is settled, keep the
198
- price in the Standard band or agree a merchant-specific contract with Siglume
199
- before launch.
196
+ guarantee provider revenue before settlement succeeds. Merchant setup and the
197
+ billing mandate terms assume the merchant accepts this Micro / Nano delayed
198
+ aggregated settlement model whenever they offer amounts in these bands. If a
199
+ product cannot fulfill before provider revenue is settled, keep the price in the
200
+ Standard band; in practice, do not offer JPY 500-and-under or USD 3-and-under
201
+ items for that product.
200
202
  Micro / Nano budget checks reserve spending capacity only; they do not lock,
201
203
  escrow, or guarantee the buyer's wallet balance, allowance, or settlement funds.
202
204
  Sub-minor-unit Nano fees are accumulated with decimal precision and rounded only
@@ -260,16 +262,19 @@ amounts differ.
260
262
  | Under JPY 50 / up to USD 0.30 | Nano Payment | Applied automatically by amount | USD 0.001 / usage, about JPY 0.2 | Monthly settlement - see [Settlement schedule](./docs/pricing.md#settlement-schedule) |
261
263
 
262
264
  A merchant billing mandate is required before accepting payments, even on the
263
- Launch plan. The current public API does not expose a flag that forces a
264
- JPY 500-and-under / USD 3-and-under payment into Standard immediate settlement.
265
- If immediate on-chain settlement is a hard requirement, price the item in the
266
- Standard band or confirm a merchant-specific contract with Siglume before
267
- launch. Public Direct Payment / Hosted Checkout `amount_minor` is a positive
268
- integer in minor currency units, so public one-time Nano amounts start at JPY 1
269
- or USD 0.01. For Standard Payment, `fee_bps` returned on a payment requirement
270
- is the authoritative fee rate for that payment in the merchant's settlement
271
- currency. For Micro / Nano, the statement APIs expose `protocol_fee_minor`,
272
- `gross_buyer_debit_minor`, `buyer_debit_minor`, and `rounding_delta_minor`.
265
+ Launch plan. The current public API chooses the payment band from
266
+ `amount_minor`; JPY 500-and-under / USD 3-and-under payments are routed to
267
+ Micro / Nano delayed aggregated settlement. Accepting the SDRP merchant terms
268
+ means accepting automatic Micro / Nano delayed aggregated settlement for those
269
+ low-price bands. If immediate on-chain settlement is a hard requirement, price
270
+ the item in the Standard band; in practice, do not offer JPY 500-and-under or
271
+ USD 3-and-under items for that product. Public Direct Payment / Hosted Checkout
272
+ `amount_minor` is a positive integer in minor currency units, so public one-time
273
+ Nano amounts start at JPY 1 or USD 0.01. For Standard Payment, `fee_bps`
274
+ returned on a payment requirement is the authoritative fee rate for that payment
275
+ in the merchant's settlement currency. For Micro / Nano, the statement APIs
276
+ expose `protocol_fee_minor`, `gross_buyer_debit_minor`, `buyer_debit_minor`, and
277
+ `rounding_delta_minor`.
273
278
  The full fee table and the weekly / monthly settlement schedule live in
274
279
  [docs/pricing.md](./docs/pricing.md). Statement APIs for "how much was used,
275
280
  when will it close, when can it debit, and what is settled" are documented in
@@ -520,7 +525,10 @@ the payload. Create a marketplace webhook subscription with
520
525
  signing secret once.
521
526
 
522
527
  ```ts
523
- import { verifyDirectRequestPaymentWebhook } from "@siglume/direct-request-payment";
528
+ import {
529
+ classifyDirectPaymentConfirmation,
530
+ verifyDirectRequestPaymentWebhook,
531
+ } from "@siglume/direct-request-payment";
524
532
 
525
533
  const { event } = await verifyDirectRequestPaymentWebhook(
526
534
  process.env.SIGLUME_WEBHOOK_SECRET!,
@@ -529,18 +537,16 @@ const { event } = await verifyDirectRequestPaymentWebhook(
529
537
  );
530
538
 
531
539
  if (event.type === "direct_payment.confirmed") {
532
- if (event.data.mode === "metered_settlement_batch") {
540
+ const confirmation = classifyDirectPaymentConfirmation(event);
541
+ if (confirmation.kind === "metered_batch_settled") {
533
542
  // Reconcile settled Micro / Nano batches by settlement_batch_id /
534
543
  // usage_event_digest; these events do not carry an order challenge hash.
535
- } else if (
536
- event.data.pricing_band === "standard" &&
537
- event.data.finality === "per_payment_onchain" &&
538
- event.data.settlement_status === "settled"
539
- ) {
544
+ } else if (confirmation.kind === "standard_settled") {
540
545
  // Mark the order paid once if event.data.challenge_hash/order mapping matches.
541
- } else if (event.data.pricing_band === "micro" || event.data.pricing_band === "nano") {
542
- // Mark fulfilled-but-unsettled only if your business allows fulfillment
543
- // before the aggregated Micro / Nano settlement succeeds.
546
+ } else if (confirmation.kind === "metered_usage_accepted") {
547
+ // Mark fulfilled-but-unsettled after matching confirmation.challenge_hash.
548
+ } else {
549
+ // Route confirmation.reason to manual review. Do not mark paid or fulfilled.
544
550
  }
545
551
  }
546
552
  ```
@@ -548,7 +554,10 @@ if (event.type === "direct_payment.confirmed") {
548
554
  ```py
549
555
  import os
550
556
 
551
- from siglume_direct_request_payment import verify_direct_request_payment_webhook
557
+ from siglume_direct_request_payment import (
558
+ classify_direct_payment_confirmation,
559
+ verify_direct_request_payment_webhook,
560
+ )
552
561
 
553
562
  verified = verify_direct_request_payment_webhook(
554
563
  os.environ["SIGLUME_WEBHOOK_SECRET"],
@@ -557,30 +566,30 @@ verified = verify_direct_request_payment_webhook(
557
566
  )
558
567
 
559
568
  if verified["event"]["type"] == "direct_payment.confirmed":
560
- data = verified["event"]["data"]
561
- if data.get("mode") == "metered_settlement_batch":
569
+ confirmation = classify_direct_payment_confirmation(verified["event"])
570
+ if confirmation["kind"] == "metered_batch_settled":
562
571
  # Reconcile settled Micro / Nano batches by settlement_batch_id /
563
572
  # usage_event_digest; these events do not carry an order challenge hash.
564
573
  pass
565
- elif (
566
- data.get("pricing_band") == "standard"
567
- and data.get("finality") == "per_payment_onchain"
568
- and data.get("settlement_status") == "settled"
569
- ):
574
+ elif confirmation["kind"] == "standard_settled":
570
575
  # Mark the order paid once if event.data.challenge_hash/order mapping matches.
571
576
  pass
572
- elif data.get("pricing_band") in ("micro", "nano"):
573
- # Mark fulfilled-but-unsettled only if your business allows fulfillment
574
- # before the aggregated Micro / Nano settlement succeeds.
577
+ elif confirmation["kind"] == "metered_usage_accepted":
578
+ # Mark fulfilled-but-unsettled after matching confirmation["challenge_hash"].
579
+ pass
580
+ else:
581
+ # Route confirmation["reason"] to manual review. Do not mark paid or fulfilled.
575
582
  pass
576
583
  ```
577
584
 
578
585
  New `direct_payment.confirmed` payloads include `pricing_band`,
579
586
  `settlement_cadence`, `finality`, `protocol_fee_minor`, `settlement_status`,
580
587
  `settlement_batch_id`, `chain_receipt_id`, `usage_event_digest`, `settled_at`,
581
- and when available `request_hash_v2`. Use these machine fields instead of
582
- inferring settlement semantics from the event name alone. Do not mark an order
583
- paid from the event type alone.
588
+ and when available `request_hash_v2`. Use
589
+ `classifyDirectPaymentConfirmation(event)` /
590
+ `classify_direct_payment_confirmation(event)` or the same machine-field checks
591
+ instead of inferring settlement semantics from the event name alone. Do not mark
592
+ an order paid from the event type alone.
584
593
 
585
594
  ## Security Rules
586
595
 
package/dist/index.cjs CHANGED
@@ -34,11 +34,15 @@ __export(src_exports, {
34
34
  DEFAULT_WEBHOOK_TOLERANCE_SECONDS: () => DEFAULT_WEBHOOK_TOLERANCE_SECONDS,
35
35
  DIRECT_REQUEST_PAYMENT_ALLOWANCE_RECEIPT_KIND: () => DIRECT_REQUEST_PAYMENT_ALLOWANCE_RECEIPT_KIND,
36
36
  DIRECT_REQUEST_PAYMENT_CHALLENGE_SCHEME: () => DIRECT_REQUEST_PAYMENT_CHALLENGE_SCHEME,
37
+ DIRECT_REQUEST_PAYMENT_METERED_ACCEPTED_STATUS: () => DIRECT_REQUEST_PAYMENT_METERED_ACCEPTED_STATUS,
38
+ DIRECT_REQUEST_PAYMENT_METERED_FINALITY: () => DIRECT_REQUEST_PAYMENT_METERED_FINALITY,
37
39
  DIRECT_REQUEST_PAYMENT_MODE: () => DIRECT_REQUEST_PAYMENT_MODE,
38
40
  DIRECT_REQUEST_PAYMENT_RECEIPT_KIND: () => DIRECT_REQUEST_PAYMENT_RECEIPT_KIND,
39
41
  DIRECT_REQUEST_PAYMENT_RECURRING_CHALLENGE_SCHEME: () => DIRECT_REQUEST_PAYMENT_RECURRING_CHALLENGE_SCHEME,
40
42
  DIRECT_REQUEST_PAYMENT_REFERENCE_TYPE: () => DIRECT_REQUEST_PAYMENT_REFERENCE_TYPE,
41
43
  DIRECT_REQUEST_PAYMENT_SDK_VERSION: () => DIRECT_REQUEST_PAYMENT_SDK_VERSION,
44
+ DIRECT_REQUEST_PAYMENT_STANDARD_FINALITY: () => DIRECT_REQUEST_PAYMENT_STANDARD_FINALITY,
45
+ DIRECT_REQUEST_PAYMENT_STANDARD_SETTLED_STATUS: () => DIRECT_REQUEST_PAYMENT_STANDARD_SETTLED_STATUS,
42
46
  DirectRequestPaymentClient: () => DirectRequestPaymentClient,
43
47
  DirectRequestPaymentMerchantClient: () => DirectRequestPaymentMerchantClient,
44
48
  HostedCheckoutNotAvailableError: () => HostedCheckoutNotAvailableError,
@@ -50,6 +54,7 @@ __export(src_exports, {
50
54
  buildPaymentExecutionPayload: () => buildPaymentExecutionPayload,
51
55
  buildPreparedTransactionExecutionPayload: () => buildPreparedTransactionExecutionPayload,
52
56
  buildWebhookSignatureHeader: () => buildWebhookSignatureHeader,
57
+ classifyDirectPaymentConfirmation: () => classifyDirectPaymentConfirmation,
53
58
  computeWebhookSignature: () => computeWebhookSignature,
54
59
  createDirectRequestPaymentChallenge: () => createDirectRequestPaymentChallenge,
55
60
  createDirectRequestPaymentChallengeSignature: () => createDirectRequestPaymentChallengeSignature,
@@ -78,7 +83,11 @@ var DIRECT_REQUEST_PAYMENT_RECEIPT_KIND = "sdrp_direct_payment";
78
83
  var DIRECT_REQUEST_PAYMENT_ALLOWANCE_RECEIPT_KIND = "sdrp_direct_payment_allowance";
79
84
  var DIRECT_REQUEST_PAYMENT_REFERENCE_TYPE = "sdrp_direct_payment_requirement";
80
85
  var DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 300;
81
- var DIRECT_REQUEST_PAYMENT_SDK_VERSION = "0.4.6";
86
+ var DIRECT_REQUEST_PAYMENT_SDK_VERSION = "0.4.8";
87
+ var DIRECT_REQUEST_PAYMENT_STANDARD_SETTLED_STATUS = "settled";
88
+ var DIRECT_REQUEST_PAYMENT_METERED_ACCEPTED_STATUS = "pending_settlement";
89
+ var DIRECT_REQUEST_PAYMENT_STANDARD_FINALITY = "per_payment_onchain";
90
+ var DIRECT_REQUEST_PAYMENT_METERED_FINALITY = "aggregated_onchain_settlement";
82
91
  var DIRECT_REQUEST_PAYMENT_CONFIRMED_WEBHOOK_MODES = /* @__PURE__ */ new Set([DIRECT_REQUEST_PAYMENT_MODE, "metered_settlement_batch"]);
83
92
  var SiglumeDirectRequestPaymentError = class extends Error {
84
93
  constructor(message) {
@@ -671,6 +680,112 @@ function parseDirectRequestPaymentWebhookEvent(payload) {
671
680
  }
672
681
  return parsed;
673
682
  }
683
+ function classifyDirectPaymentConfirmation(event) {
684
+ const data = event.data;
685
+ const requirementId = stringOrNull(data.requirement_id) ?? stringOrNull(data.direct_payment_requirement_id);
686
+ const challengeHash = stringOrNull(data.challenge_hash);
687
+ const pricingBand = stringOrNull(data.pricing_band);
688
+ const finality = stringOrNull(data.finality);
689
+ const settlementStatus = stringOrNull(data.settlement_status);
690
+ if (event.type !== "direct_payment.confirmed") {
691
+ return {
692
+ kind: "unknown",
693
+ event,
694
+ data,
695
+ reason: "not_direct_payment_confirmed",
696
+ requirement_id: requirementId,
697
+ settlement_batch_id: stringOrNull(data.settlement_batch_id),
698
+ pricing_band: pricingBand,
699
+ settlement_status: settlementStatus,
700
+ finality
701
+ };
702
+ }
703
+ if (data.mode === "metered_settlement_batch") {
704
+ const settlementBatchId = stringOrNull(data.settlement_batch_id);
705
+ const chainReceiptId = stringOrNull(data.chain_receipt_id);
706
+ const usageEventDigest = stringOrNull(data.usage_event_digest);
707
+ if (settlementStatus === DIRECT_REQUEST_PAYMENT_STANDARD_SETTLED_STATUS && settlementBatchId && chainReceiptId && usageEventDigest) {
708
+ return {
709
+ kind: "metered_batch_settled",
710
+ event,
711
+ data,
712
+ settlement_batch_id: settlementBatchId,
713
+ chain_receipt_id: chainReceiptId,
714
+ usage_event_digest: usageEventDigest,
715
+ settled_at: stringOrNull(data.settled_at)
716
+ };
717
+ }
718
+ return {
719
+ kind: "unknown",
720
+ event,
721
+ data,
722
+ reason: "invalid_metered_settlement_confirmation",
723
+ requirement_id: requirementId,
724
+ settlement_batch_id: settlementBatchId,
725
+ pricing_band: pricingBand,
726
+ settlement_status: settlementStatus,
727
+ finality
728
+ };
729
+ }
730
+ if (pricingBand === "standard") {
731
+ const chainReceiptId = stringOrNull(data.chain_receipt_id);
732
+ if (finality === DIRECT_REQUEST_PAYMENT_STANDARD_FINALITY && settlementStatus === DIRECT_REQUEST_PAYMENT_STANDARD_SETTLED_STATUS && requirementId && challengeHash && chainReceiptId) {
733
+ return {
734
+ kind: "standard_settled",
735
+ event,
736
+ data,
737
+ requirement_id: requirementId,
738
+ challenge_hash: challengeHash,
739
+ chain_receipt_id: chainReceiptId,
740
+ request_hash_v2: stringOrNull(data.request_hash_v2)
741
+ };
742
+ }
743
+ return {
744
+ kind: "unknown",
745
+ event,
746
+ data,
747
+ reason: "missing_standard_settlement_fields",
748
+ requirement_id: requirementId,
749
+ pricing_band: pricingBand,
750
+ settlement_status: settlementStatus,
751
+ finality
752
+ };
753
+ }
754
+ if (pricingBand === "micro" || pricingBand === "nano") {
755
+ if (finality === DIRECT_REQUEST_PAYMENT_METERED_FINALITY && settlementStatus === DIRECT_REQUEST_PAYMENT_METERED_ACCEPTED_STATUS && requirementId && challengeHash) {
756
+ return {
757
+ kind: "metered_usage_accepted",
758
+ event,
759
+ data,
760
+ pricing_band: pricingBand,
761
+ requirement_id: requirementId,
762
+ challenge_hash: challengeHash,
763
+ request_hash_v2: stringOrNull(data.request_hash_v2)
764
+ };
765
+ }
766
+ return {
767
+ kind: "unknown",
768
+ event,
769
+ data,
770
+ reason: "missing_metered_usage_fields",
771
+ requirement_id: requirementId,
772
+ pricing_band: pricingBand,
773
+ settlement_status: settlementStatus,
774
+ finality
775
+ };
776
+ }
777
+ return {
778
+ kind: "unknown",
779
+ event,
780
+ data,
781
+ reason: "unknown_confirmation_shape",
782
+ requirement_id: requirementId,
783
+ settlement_batch_id: stringOrNull(data.settlement_batch_id),
784
+ pricing_band: pricingBand,
785
+ settlement_status: settlementStatus,
786
+ finality
787
+ };
788
+ }
674
789
  async function verifyDirectRequestPaymentWebhook(signing_secret, body, signature_header, options = {}) {
675
790
  const verification = await verifyWebhookSignature(signing_secret, body, signature_header, options);
676
791
  const text = new TextDecoder().decode(bodyBytes(body));