@siglume/direct-request-payment 0.3.0 → 0.3.2
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 +61 -50
- package/LICENSE +21 -21
- package/README.md +424 -404
- package/dist/index.cjs +6 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -6
- package/dist/index.d.ts +9 -6
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/docs/announcement-ja.md +81 -64
- package/docs/api-reference.md +335 -274
- package/docs/merchant-quickstart.md +302 -296
- package/docs/pricing.md +93 -73
- package/docs/security.md +114 -99
- package/examples/express-checkout.ts +106 -106
- package/examples/setup-merchant.ts +17 -17
- package/package.json +71 -71
|
@@ -1,296 +1,302 @@
|
|
|
1
|
-
# Merchant Quickstart
|
|
2
|
-
|
|
3
|
-
This guide shows the minimum safe Siglume Direct Request Payment flow for an
|
|
4
|
-
external merchant.
|
|
5
|
-
|
|
6
|
-
## Actors
|
|
7
|
-
|
|
8
|
-
- Merchant server: owns the order, amount, currency, challenge secret, webhook
|
|
9
|
-
endpoint, and order fulfillment.
|
|
10
|
-
- Buyer: owns the Siglume wallet that pays the DirectPaymentHub transaction.
|
|
11
|
-
- Siglume: creates the payment requirement, prepares the wallet transaction,
|
|
12
|
-
verifies the receipt, and emits signed webhooks.
|
|
13
|
-
|
|
14
|
-
The merchant server must not create charges with a customer wallet. It signs the
|
|
15
|
-
order challenge; the buyer-facing Siglume payment flow pays it.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
amount_minor:
|
|
95
|
-
currency:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
order["
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
- `
|
|
278
|
-
|
|
279
|
-
- `
|
|
280
|
-
|
|
281
|
-
- `
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
- `
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
-
|
|
292
|
-
|
|
293
|
-
-
|
|
294
|
-
-
|
|
295
|
-
-
|
|
296
|
-
|
|
1
|
+
# Merchant Quickstart
|
|
2
|
+
|
|
3
|
+
This guide shows the minimum safe Siglume Direct Request Payment flow for an
|
|
4
|
+
external merchant.
|
|
5
|
+
|
|
6
|
+
## Actors
|
|
7
|
+
|
|
8
|
+
- Merchant server: owns the order, amount, currency, challenge secret, webhook
|
|
9
|
+
endpoint, and order fulfillment.
|
|
10
|
+
- Buyer: owns the Siglume wallet that pays the DirectPaymentHub transaction.
|
|
11
|
+
- Siglume: creates the payment requirement, prepares the wallet transaction,
|
|
12
|
+
verifies the receipt, and emits signed webhooks.
|
|
13
|
+
|
|
14
|
+
The merchant server must not create charges with a customer wallet. It signs the
|
|
15
|
+
order challenge; the buyer-facing Siglume payment flow pays it.
|
|
16
|
+
|
|
17
|
+
This quickstart is for SDRP Standard Payment in an external merchant product. If
|
|
18
|
+
you are publishing a Siglume API Store listing that should use Micro Payment or
|
|
19
|
+
Nano Payment, use the API Store flow instead. Micro/Nano run a server-side meter
|
|
20
|
+
gate before provider execution and settle later; they are not browser checkout
|
|
21
|
+
requirements created by this merchant SDK.
|
|
22
|
+
|
|
23
|
+
## 1. Run Merchant Setup
|
|
24
|
+
|
|
25
|
+
Run setup from the merchant server, CI, or an integration agent with the
|
|
26
|
+
merchant's Siglume JWT. Do not use a Developer Portal `cli_` key here.
|
|
27
|
+
|
|
28
|
+
TypeScript:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { DirectRequestPaymentMerchantClient } from "@siglume/direct-request-payment";
|
|
32
|
+
|
|
33
|
+
const merchantClient = new DirectRequestPaymentMerchantClient({
|
|
34
|
+
auth_token: process.env.SIGLUME_MERCHANT_AUTH_TOKEN!,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const setup = await merchantClient.setupCheckout({
|
|
38
|
+
merchant: "example_merchant",
|
|
39
|
+
display_name: "Example Merchant",
|
|
40
|
+
billing_plan: "launch",
|
|
41
|
+
billing_currency: "JPY",
|
|
42
|
+
webhook_callback_url: "https://merchant.example/siglume/webhook",
|
|
43
|
+
max_amount_minor: 100000,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
console.log(setup.env);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Python:
|
|
50
|
+
|
|
51
|
+
```py
|
|
52
|
+
import os
|
|
53
|
+
|
|
54
|
+
from siglume_direct_request_payment import DirectRequestPaymentMerchantClient
|
|
55
|
+
|
|
56
|
+
merchant_client = DirectRequestPaymentMerchantClient(
|
|
57
|
+
auth_token=os.environ["SIGLUME_MERCHANT_AUTH_TOKEN"],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
setup = merchant_client.setup_checkout(
|
|
61
|
+
merchant="example_merchant",
|
|
62
|
+
display_name="Example Merchant",
|
|
63
|
+
billing_plan="launch",
|
|
64
|
+
billing_currency="JPY",
|
|
65
|
+
webhook_callback_url="https://merchant.example/siglume/webhook",
|
|
66
|
+
max_amount_minor=100000,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
print(setup["env"])
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`setupCheckout` / `setup_checkout` performs:
|
|
73
|
+
|
|
74
|
+
- merchant key claim
|
|
75
|
+
- challenge secret creation
|
|
76
|
+
- billing mandate preparation
|
|
77
|
+
- webhook subscription creation for `direct_payment.confirmed` and
|
|
78
|
+
`direct_payment.spent`
|
|
79
|
+
|
|
80
|
+
Store `SIGLUME_DIRECT_PAYMENT_CHALLENGE_SECRET` and `SIGLUME_WEBHOOK_SECRET`
|
|
81
|
+
server-side only. Secrets are returned only when they are created or rotated.
|
|
82
|
+
If the returned billing mandate requires wallet approval, complete that Siglume
|
|
83
|
+
wallet step before accepting production payments.
|
|
84
|
+
|
|
85
|
+
## 2. Create an Order and Challenge
|
|
86
|
+
|
|
87
|
+
The merchant server creates the order before asking Siglume for payment.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { createDirectRequestPaymentChallenge } from "@siglume/direct-request-payment";
|
|
91
|
+
|
|
92
|
+
const order = {
|
|
93
|
+
id: "order_123",
|
|
94
|
+
amount_minor: 1200,
|
|
95
|
+
currency: "JPY",
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const challenge = await createDirectRequestPaymentChallenge({
|
|
99
|
+
merchant: "example_merchant",
|
|
100
|
+
amount_minor: order.amount_minor,
|
|
101
|
+
currency: order.currency,
|
|
102
|
+
secret: process.env.SIGLUME_DIRECT_PAYMENT_CHALLENGE_SECRET!,
|
|
103
|
+
nonce: `${order.id}-attempt_1`,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await orders.update(order.id, {
|
|
107
|
+
siglume_challenge_hash: challenge.challenge_hash,
|
|
108
|
+
siglume_payment_status: "pending",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
order_id: order.id,
|
|
113
|
+
amount_minor: order.amount_minor,
|
|
114
|
+
currency: order.currency,
|
|
115
|
+
siglume_challenge: challenge.challenge,
|
|
116
|
+
};
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Python:
|
|
120
|
+
|
|
121
|
+
```py
|
|
122
|
+
import os
|
|
123
|
+
|
|
124
|
+
from siglume_direct_request_payment import create_direct_request_payment_challenge
|
|
125
|
+
|
|
126
|
+
order = {
|
|
127
|
+
"id": "order_123",
|
|
128
|
+
"amount_minor": 1200,
|
|
129
|
+
"currency": "JPY",
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
challenge = create_direct_request_payment_challenge(
|
|
133
|
+
merchant="example_merchant",
|
|
134
|
+
amount_minor=order["amount_minor"],
|
|
135
|
+
currency=order["currency"],
|
|
136
|
+
secret=os.environ["SIGLUME_DIRECT_PAYMENT_CHALLENGE_SECRET"],
|
|
137
|
+
nonce=f"{order['id']}-attempt_1",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
orders.update(
|
|
141
|
+
order["id"],
|
|
142
|
+
{
|
|
143
|
+
"siglume_challenge_hash": challenge["challenge_hash"],
|
|
144
|
+
"siglume_payment_status": "pending",
|
|
145
|
+
},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
"order_id": order["id"],
|
|
150
|
+
"amount_minor": order["amount_minor"],
|
|
151
|
+
"currency": order["currency"],
|
|
152
|
+
"siglume_challenge": challenge["challenge"],
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Never calculate `amount_minor` from browser input.
|
|
157
|
+
The nonce must be unique per order payment attempt and must not contain `:`.
|
|
158
|
+
|
|
159
|
+
## 3. Buyer Creates and Pays the Requirement
|
|
160
|
+
|
|
161
|
+
After the buyer authenticates with Siglume, create the payment requirement with
|
|
162
|
+
the buyer's Siglume bearer token. Do not use a Developer Portal `cli_` API key
|
|
163
|
+
or merchant API key here.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { DirectRequestPaymentClient } from "@siglume/direct-request-payment";
|
|
167
|
+
|
|
168
|
+
const siglume = new DirectRequestPaymentClient({
|
|
169
|
+
auth_token: buyerSiglumeBearerToken,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const requirement = await siglume.createPaymentRequirement({
|
|
173
|
+
merchant: "example_merchant",
|
|
174
|
+
amount_minor: order.amount_minor,
|
|
175
|
+
currency: order.currency,
|
|
176
|
+
challenge: order.siglume_challenge,
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Python:
|
|
181
|
+
|
|
182
|
+
```py
|
|
183
|
+
from siglume_direct_request_payment import DirectRequestPaymentClient
|
|
184
|
+
|
|
185
|
+
siglume = DirectRequestPaymentClient(auth_token=buyer_siglume_bearer_token)
|
|
186
|
+
|
|
187
|
+
requirement = siglume.create_payment_requirement(
|
|
188
|
+
merchant="example_merchant",
|
|
189
|
+
amount_minor=order["amount_minor"],
|
|
190
|
+
currency=order["currency"],
|
|
191
|
+
challenge=order["siglume_challenge"],
|
|
192
|
+
)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
If Siglume returns `approve_transaction_request`, execute it first. Then execute
|
|
196
|
+
the payment transaction and verify the receipt.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
if (requirement.approve_transaction_request) {
|
|
200
|
+
await siglume.executeAllowanceTransaction(requirement, { await_finality: true });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const payment = await siglume.executePaymentTransaction(requirement, {
|
|
204
|
+
await_finality: true,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await siglume.verifyPaymentRequirement(requirement.requirement_id, {
|
|
208
|
+
receipt_id: String(payment.receipt?.receipt_id ?? ""),
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Python:
|
|
213
|
+
|
|
214
|
+
```py
|
|
215
|
+
if requirement.get("approve_transaction_request"):
|
|
216
|
+
siglume.execute_allowance_transaction(requirement, await_finality=True)
|
|
217
|
+
|
|
218
|
+
payment = siglume.execute_payment_transaction(requirement, await_finality=True)
|
|
219
|
+
|
|
220
|
+
siglume.verify_payment_requirement(
|
|
221
|
+
requirement["requirement_id"],
|
|
222
|
+
receipt_id=str((payment.get("receipt") or {}).get("receipt_id") or ""),
|
|
223
|
+
)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## 4. Fulfill from Webhook
|
|
227
|
+
|
|
228
|
+
Use the webhook as the durable signal, not just the browser return path.
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
import { verifyDirectRequestPaymentWebhook } from "@siglume/direct-request-payment";
|
|
232
|
+
|
|
233
|
+
const { event } = await verifyDirectRequestPaymentWebhook(
|
|
234
|
+
process.env.SIGLUME_WEBHOOK_SECRET!,
|
|
235
|
+
rawRequestBody,
|
|
236
|
+
siglumeSignatureHeader,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (event.type === "direct_payment.confirmed") {
|
|
240
|
+
const data = event.data;
|
|
241
|
+
const order = await orders.findByChallengeHash(String(data.challenge_hash ?? ""));
|
|
242
|
+
if (!order) {
|
|
243
|
+
throw new Error("Unknown Siglume challenge hash");
|
|
244
|
+
}
|
|
245
|
+
await orders.markPaidOnce(order.id, {
|
|
246
|
+
siglume_requirement_id: String(data.requirement_id ?? data.direct_payment_requirement_id ?? ""),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Python:
|
|
252
|
+
|
|
253
|
+
```py
|
|
254
|
+
import os
|
|
255
|
+
|
|
256
|
+
from siglume_direct_request_payment import verify_direct_request_payment_webhook
|
|
257
|
+
|
|
258
|
+
verified = verify_direct_request_payment_webhook(
|
|
259
|
+
os.environ["SIGLUME_WEBHOOK_SECRET"],
|
|
260
|
+
raw_request_body,
|
|
261
|
+
siglume_signature_header,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if verified["event"]["type"] == "direct_payment.confirmed":
|
|
265
|
+
data = verified["event"]["data"]
|
|
266
|
+
order = orders.find_by_challenge_hash(str(data.get("challenge_hash") or ""))
|
|
267
|
+
if not order:
|
|
268
|
+
raise RuntimeError("Unknown Siglume challenge hash")
|
|
269
|
+
orders.mark_paid_once(
|
|
270
|
+
order["id"],
|
|
271
|
+
siglume_requirement_id=str(data.get("requirement_id") or data.get("direct_payment_requirement_id") or ""),
|
|
272
|
+
)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Failure Handling
|
|
276
|
+
|
|
277
|
+
- `EXTERNAL_402_CHALLENGE_REQUIRED`: the merchant server did not provide a
|
|
278
|
+
challenge.
|
|
279
|
+
- `INVALID_EXTERNAL_402_CHALLENGE`: the amount, currency, merchant, nonce, or
|
|
280
|
+
signature does not match.
|
|
281
|
+
- `EXTERNAL_402_CHALLENGE_ALREADY_USED`: the challenge is already bound to a
|
|
282
|
+
different buyer.
|
|
283
|
+
- `EXTERNAL_402_MERCHANT_NOT_FOUND`: run merchant setup with the merchant's
|
|
284
|
+
Siglume JWT.
|
|
285
|
+
- `EXTERNAL_402_MERCHANT_BILLING_SETUP_REQUIRED`: the merchant billing mandate
|
|
286
|
+
is not active yet.
|
|
287
|
+
- `EXTERNAL_402_MERCHANT_BILLING_PAST_DUE` or
|
|
288
|
+
`EXTERNAL_402_MERCHANT_BILLING_SUSPENDED`: merchant billing must be fixed
|
|
289
|
+
before new payments can be accepted.
|
|
290
|
+
|
|
291
|
+
## Go-Live Checklist
|
|
292
|
+
|
|
293
|
+
- `setupCheckout` / `setup_checkout` has claimed the merchant key.
|
|
294
|
+
- Merchant billing mandate is active.
|
|
295
|
+
- Challenge secret is only in server-side environment variables.
|
|
296
|
+
- Webhook endpoint receives raw body and verifies `Siglume-Signature`.
|
|
297
|
+
- Orders store `challenge_hash`, `requirement_id`, and fulfillment status.
|
|
298
|
+
- Fulfillment is idempotent.
|
|
299
|
+
- Browser input cannot change the amount or currency.
|
|
300
|
+
- Nonces cannot be reused for separate order attempts.
|
|
301
|
+
- The order is fulfilled only after a verified webhook maps back to the stored
|
|
302
|
+
`challenge_hash`.
|