@mindtdilly / Omnipay-3 / commits / 89a25c1

docs: add eth_wallet funding method alongside ACH

Document eth_wallet (0x address, optional ENS, network) on POST /funding-method, extend integrate-checkout + picker guide, and add a fourth ETH wallet tile to the live checkout picker with client-side hex address validation.

mindtdilly committed Sep 29, 2026 at 01:59 UTC 89a25c14c4e62dab1acc3d9b88333406928ff1f0
7 files changed +256 -17
api/sessions-funding-method.mdx
+139 -10
@@ -1,10 +1,10 @@
1 ---
2 title: "Funding Method — Bind Payment to Procurement Session"
3 sidebarTitle: "POST /funding-method"
4 -description: "Bind a payment method to an active procurement session. Accepts USDC on-chain, card, or ACH bank transfer. Replaces the deprecated POST /api/payments/route."
4 +description: "Bind a payment method to an active procurement session. Accepts USDC on-chain, card, ACH bank transfer, or ETH wallet. Replaces the deprecated POST /api/payments/route."
5 ---
6
7 -Once a procurement session is ready for payment, this endpoint binds a funding method to it. You specify the method type — USDC, card, or bank transfer (ACH) — and supply method-specific fields. For USDC payments, include the target blockchain network. For ACH bank transfers, include routing and account details. ProcureNet validates the combination against the session's state and the buyer's entitlements, then advances the session toward payment processing. This endpoint replaces the deprecated `POST /api/payments/route` — if your integration still uses that path, follow the migration guidance at the bottom of this page.
7 +Once a procurement session is ready for payment, this endpoint binds a funding method to it. You specify the method type — USDC, card, bank transfer (ACH), or ETH wallet — and supply method-specific fields. For USDC and ETH wallet payments, include the target blockchain network. For ACH bank transfers, include routing and account details. For ETH wallet, include the `0x` address (optional ENS). ProcureNet validates the combination against the session's state and the buyer's entitlements, then advances the session toward payment processing. This endpoint replaces the deprecated `POST /api/payments/route` — if your integration still uses that path, follow the migration guidance at the bottom of this page.
8
9 ## Authentication
10
@@ -32,21 +32,22 @@ Authorization: Bearer <your_jwt>
32 | `usdc` | On-chain USDC transfer. Requires the `network` field. |
33 | `card` | Credit or debit card. Network and ACH fields are ignored. |
34 | `bank_transfer` | ACH debit from a US bank account. Requires ACH fields below. Network is ignored. |
35 + | `eth_wallet` | Native ETH from a buyer wallet. Requires `wallet_address` and `network`. Optional `ens_name`. |
36 </ParamField>
37
38 <ParamField body="network" type="string">
38 - The blockchain network for USDC transfers. Required when `method` is `"usdc"`. Ignored for all other methods.
39 + The blockchain network for on-chain funding. Required when `method` is `"usdc"` or `"eth_wallet"`. Ignored for `card` and `bank_transfer`.
40
41 Supported values: `base`, `arbitrum`, `polygon`, `ethereum`.
42
43 <Tip>
43 - `base` offers the lowest gas fees for most USDC transfers and is the recommended network unless your buyer or contract rules require otherwise.
44 + For USDC, `base` offers the lowest gas fees and is recommended unless your buyer or contract rules require otherwise. For native ETH (`eth_wallet`), default to `ethereum` unless the buyer wallet is L2-native.
45 </Tip>
46 </ParamField>
47
48 ### ACH fields (`method: "bank_transfer"`)
49
49 -When `method` is `"bank_transfer"`, include the following fields. They are ignored for `usdc` and `card`.
50 +When `method` is `"bank_transfer"`, include the following fields. They are ignored for `usdc`, `card`, and `eth_wallet`.
51
52 <ParamField body="routing_number" type="string" required>
53 ABA routing transit number for the buyer's US bank. Must be exactly **9 digits**. Leading zeros are significant — send as a string, not a number.
@@ -79,6 +80,31 @@ When `method` is `"bank_transfer"`, include the following fields. They are ignor
80 ACH credentials are transmitted over TLS and stored only as needed to originate the debit. Full account numbers are never returned in API responses — only a masked last-four form is echoed.
81 </Info>
82
83 +
84 +### ETH wallet fields (`method: "eth_wallet"`)
85 +
86 +When `method` is `"eth_wallet"`, include the following fields. They are ignored for `usdc`, `card`, and `bank_transfer`.
87 +
88 +<ParamField body="wallet_address" type="string" required>
89 + Buyer EVM wallet address that will fund the session with native ETH. Must be a checksum-compatible hex address: `0x` followed by exactly **40** hexadecimal characters (`/^0x[a-fA-F0-9]{40}$/`). Case may be mixed (EIP-55); the API normalizes to lowercase for storage and returns the submitted casing when valid.
90 +
91 + Example: `"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0"`
92 +</ParamField>
93 +
94 +<ParamField body="ens_name" type="string">
95 + Optional ENS name associated with the wallet (for example `"acme.eth"`). Display and verification aid only — settlement always uses `wallet_address`. If both are provided, the resolved ENS address must match `wallet_address` or the request fails with `422`.
96 +
97 + Example: `"treasury.acme.eth"`
98 +</ParamField>
99 +
100 +<ParamField body="network" type="string" required>
101 + Chain for the ETH debit. Same enum as USDC: `base`, `arbitrum`, `polygon`, `ethereum`. Prefer `ethereum` for native mainnet ETH unless the buyer wallet is L2-native.
102 +</ParamField>
103 +
104 +<Info>
105 + ProcureNet never asks for a private key or seed phrase. The buyer signs or authorizes the transfer in their wallet after the funding method is bound. Only the address (and optional ENS) are stored on the session.
106 +</Info>
107 +
108 ## Request Examples
109
110 <CodeGroup>
@@ -148,6 +174,29 @@ const res = await fetch(
174 const result = await res.json();
175 ```
176
177 +```typescript ETH Wallet
178 +const sessionId = 'sess_abc123';
179 +
180 +const res = await fetch(
181 + `https://api.procurenet.io/api/v7/sessions/${sessionId}/funding-method`,
182 + {
183 + method: 'POST',
184 + headers: {
185 + 'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...',
186 + 'Content-Type': 'application/json'
187 + },
188 + body: JSON.stringify({
189 + method: 'eth_wallet',
190 + wallet_address: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0',
191 + ens_name: 'treasury.acme.eth', // optional
192 + network: 'ethereum'
193 + })
194 + }
195 +);
196 +
197 +const result = await res.json();
198 +```
199 +
200 </CodeGroup>
201
202 ### cURL — ACH
@@ -166,6 +215,22 @@ curl -X POST \
215 }'
216 ```
217
218 +
219 +### cURL — ETH wallet
220 +
221 +```bash
222 +curl -X POST \
223 + https://api.procurenet.io/api/v7/sessions/sess_abc123/funding-method \
224 + -H "Authorization: Bearer $JWT" \
225 + -H "Content-Type: application/json" \
226 + -d '{
227 + "method": "eth_wallet",
228 + "wallet_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
229 + "ens_name": "treasury.acme.eth",
230 + "network": "ethereum"
231 + }'
232 +```
233 +
234 ## Response Fields
235
236 <ResponseField name="session_id" type="string" required>
@@ -173,11 +238,11 @@ curl -X POST \
238 </ResponseField>
239
240 <ResponseField name="method" type="string" required>
176 - Confirms the payment method that was bound to the session (`usdc`, `card`, or `bank_transfer`).
241 + Confirms the payment method that was bound to the session (`usdc`, `card`, `bank_transfer`, or `eth_wallet`).
242 </ResponseField>
243
244 <ResponseField name="network" type="string">
180 - The blockchain network confirmed for USDC transfers. Present only when `method` is `"usdc"`.
245 + The blockchain network confirmed for on-chain funding. Present when `method` is `"usdc"` or `"eth_wallet"`.
246 </ResponseField>
247
248 <ResponseField name="status" type="string" required>
@@ -211,6 +276,19 @@ curl -X POST \
276 | `returned` | Debit returned (insufficient funds, invalid account, etc.). |
277 </ResponseField>
278
279 +
280 +<ResponseField name="wallet_address" type="string">
281 + Echo of the submitted wallet address. Present only when `method` is `"eth_wallet"`.
282 +</ResponseField>
283 +
284 +<ResponseField name="wallet_address_short" type="string">
285 + Short display form (`0x` + first 4 + `…` + last 4). Present only when `method` is `"eth_wallet"`.
286 +</ResponseField>
287 +
288 +<ResponseField name="ens_name" type="string">
289 + Echo of the submitted ENS name when provided. Present only when `method` is `"eth_wallet"` and `ens_name` was sent.
290 +</ResponseField>
291 +
292 ## Response Examples
293
294 <CodeGroup>
@@ -245,6 +323,18 @@ curl -X POST \
323 }
324 ```
325
326 +```json ETH Wallet
327 +{
328 + "session_id": "sess_abc123",
329 + "method": "eth_wallet",
330 + "network": "ethereum",
331 + "status": "payment_processing",
332 + "wallet_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
333 + "wallet_address_short": "0x742d…bEb0",
334 + "ens_name": "treasury.acme.eth"
335 +}
336 +```
337 +
338 </CodeGroup>
339
340 ## ACH Validation Rules
@@ -260,7 +350,20 @@ curl -X POST \
350 Do not send routing or account numbers as JSON numbers. Leading zeros in ABA routing numbers are significant; always use strings.
351 </Warning>
352
263 -## Supported Networks for USDC
353 +
354 +## ETH Wallet Validation Rules
355 +
356 +| Field | Rule |
357 +|---|---|
358 +| `wallet_address` | Required. Must match `/^0x[a-fA-F0-9]{40}$/`. Reject all-zero address (`0x` + 40 zeros). |
359 +| `ens_name` | Optional. If present: 3–255 chars, valid ENS-like label (letters, digits, hyphens, dots), typically ending in `.eth`. |
360 +| `network` | Required. One of `base`, `arbitrum`, `polygon`, `ethereum`. |
361 +
362 +<Warning>
363 + Never collect or transmit private keys, seed phrases, or wallet passwords through this endpoint. Only the public address (and optional ENS) belong in the request body.
364 +</Warning>
365 +
366 +## Supported Networks (USDC & ETH wallet)
367
368 <CardGroup cols={2}>
369 <Card title="Base" icon="circle-check">
@@ -326,6 +429,17 @@ Replace your existing single-call payment routing with the two-step v7 session p
429 account_holder_name: 'Acme Procurement LLC'
430 })
431 });
432 +
433 + // ETH wallet
434 + await fetch(`/api/v7/sessions/${session.session_id}/funding-method`, {
435 + method: 'POST',
436 + headers: { 'Authorization': `Bearer ${jwt}` },
437 + body: JSON.stringify({
438 + method: 'eth_wallet',
439 + wallet_address: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0',
440 + network: 'ethereum'
441 + })
442 + });
443 ```
444 </Step>
445 </Steps>
@@ -334,13 +448,13 @@ Replace your existing single-call payment routing with the two-step v7 session p
448
449 | HTTP Status | Description |
450 |---|---|
337 -| `400 Bad Request` | Missing required fields; unrecognized `method`; `method` is `"usdc"` but `network` is absent or invalid; or `method` is `"bank_transfer"` with invalid ACH fields (bad routing check digit, wrong length, unknown `account_type`, empty holder name). |
451 +| `400 Bad Request` | Missing required fields; unrecognized `method`; `method` is `"usdc"` or `"eth_wallet"` but `network` is absent or invalid; `method` is `"bank_transfer"` with invalid ACH fields; or `method` is `"eth_wallet"` with an invalid `wallet_address` / `ens_name`. |
452 | `401 Unauthorized` | Missing, expired, or invalid Bearer JWT. |
453 | `403 Forbidden` | The session state does not permit setting a funding method at this point (for example, escrow acceptance is still required). |
454 | `404 Not Found` | No session exists for the provided `id`. |
455 | `409 Conflict` | A funding method is already set for this session. Transition to a new session if you need to change it. |
456 | `410 Gone` | The session has expired. Start a new session with `POST /api/v7/orchestrate`. |
343 -| `422 Unprocessable Entity` | ACH details failed bank-side pre-validation (for example, routing number not found in the ABA directory). |
457 +| `422 Unprocessable Entity` | ACH details failed bank-side pre-validation (for example, routing number not found in the ABA directory); or ENS name does not resolve to the submitted `wallet_address`. |
458
459 ### ACH-specific error body example
460
@@ -355,3 +469,18 @@ Replace your existing single-call payment routing with the two-step v7 session p
469 ```
470
471 Common ACH field error codes: `invalid_routing_number`, `invalid_account_number`, `invalid_account_type`, `invalid_account_holder_name`, `missing_ach_fields`.
472 +
473 +### ETH wallet-specific error body example
474 +
475 +```json
476 +{
477 + "error": {
478 + "code": "invalid_wallet_address",
479 + "message": "wallet_address must be 0x followed by 40 hex characters",
480 + "field": "wallet_address"
481 + }
482 +}
483 +```
484 +
485 +Common ETH wallet field error codes: `invalid_wallet_address`, `invalid_ens_name`, `ens_address_mismatch`, `missing_eth_wallet_fields`, `invalid_network`.
486 +
examples/checkout-funding-picker/README.md
+3 -2
@@ -7,8 +7,9 @@ Supports:
7 - **USDC** — with network select (`base`, `arbitrum`, `polygon`, `ethereum`)
8 - **Card** — binds `method: "card"`
9 - **ACH** (`bank_transfer`) — `routing_number`, `account_number`, `account_type`, `account_holder_name`
10 +- **ETH wallet** (`eth_wallet`) — `wallet_address` (`0x` + 40 hex), optional `ens_name`, `network` (default `ethereum`)
11
11 -On submit, the page validates client-side (including ABA check digit for routing numbers), shows the request JSON, and by default performs a **live** `fetch` POST. Enable **Preview only** to skip the network call.
12 +On submit, the page validates client-side (ABA check digit for ACH; basic hex address for ETH), shows the request JSON, and by default performs a **live** `fetch` POST. Enable **Preview only** to skip the network call.
13
14 ## Config (localStorage)
15
@@ -51,7 +52,7 @@ python3 -m http.server 8765 --directory examples/checkout-funding-picker
52 |---|---|
53 | `index.html` | Markup, config fields, form structure |
54 | `styles.css` | Institutional paper / navy styling |
54 -| `app.js` | Config persistence, ACH validation, live POST, response panel |
55 +| `app.js` | Config persistence, ACH/ETH validation, live POST, response panel |
56
57 ## Related docs
58
examples/checkout-funding-picker/app.js
+46
@@ -88,6 +88,21 @@
88 formError.textContent = message;
89 }
90
91 +
92 + function isValidEthAddress(addr) {
93 + if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) return false;
94 + // Reject all-zero address
95 + if (/^0x0{40}$/i.test(addr)) return false;
96 + return true;
97 + }
98 +
99 + function isValidEnsName(name) {
100 + if (!name) return true;
101 + if (name.length < 3 || name.length > 255) return false;
102 + // Basic ENS-like: labels separated by dots, optional trailing .eth
103 + return /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/i.test(name);
104 + }
105 +
106 function abaCheckDigitValid(routing) {
107 if (!/^\d{9}$/.test(routing)) return false;
108 const d = routing.split('').map(Number);
@@ -111,6 +126,34 @@
126 return { ok: true, body };
127 }
128
129 + if (method === 'eth_wallet') {
130 + const wallet_address = document.getElementById('wallet_address').value.trim();
131 + const ens_name = document.getElementById('ens_name').value.trim();
132 + const network = document.getElementById('eth_network').value;
133 +
134 + if (!isValidEthAddress(wallet_address)) {
135 + return {
136 + ok: false,
137 + error: 'wallet_address must be 0x followed by exactly 40 hex characters (non-zero).',
138 + };
139 + }
140 + if (ens_name && !isValidEnsName(ens_name)) {
141 + return {
142 + ok: false,
143 + error: 'ens_name must be a valid ENS-like name (3–255 chars, labels and dots).',
144 + };
145 + }
146 + const networks = ['base', 'arbitrum', 'polygon', 'ethereum'];
147 + if (!networks.includes(network)) {
148 + return { ok: false, error: 'network must be base, arbitrum, polygon, or ethereum.' };
149 + }
150 +
151 + body.wallet_address = wallet_address;
152 + body.network = network;
153 + if (ens_name) body.ens_name = ens_name;
154 + return { ok: true, body };
155 + }
156 +
157 const routing_number = document.getElementById('routing_number').value.trim();
158 const account_number = document.getElementById('account_number').value.trim();
159 const account_type = document.getElementById('account_type').value;
@@ -296,6 +339,9 @@
339 document.getElementById('account_number').value = '';
340 document.getElementById('account_type').value = 'checking';
341 document.getElementById('account_holder_name').value = '';
342 + document.getElementById('wallet_address').value = '';
343 + document.getElementById('ens_name').value = '';
344 + document.getElementById('eth_network').value = 'ethereum';
345 showPanel('usdc');
346 setError('');
347 payloadBadge.textContent = 'Idle';
examples/checkout-funding-picker/index.html
+32
@@ -69,6 +69,13 @@
69 <span class="method-sub">US bank transfer</span>
70 </span>
71 </label>
72 + <label class="method-option">
73 + <input type="radio" name="method" value="eth_wallet" />
74 + <span class="method-face">
75 + <span class="method-title">ETH wallet</span>
76 + <span class="method-sub">Native ether</span>
77 + </span>
78 + </label>
79 </div>
80 </section>
81
@@ -116,6 +123,31 @@
123 <p class="hint">Send routing and account numbers as strings so leading zeros are preserved. Full account numbers are never returned by the API.</p>
124 </section>
125
126 +
127 + <section class="section panel" id="panel-eth" data-panel="eth_wallet" hidden>
128 + <h3>ETH wallet</h3>
129 + <div class="field-grid">
130 + <label class="field field-span">
131 + <span>Wallet address <em>(0x + 40 hex)</em></span>
132 + <input id="wallet_address" name="wallet_address" autocomplete="off" spellcheck="false" maxlength="42" placeholder="0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0" />
133 + </label>
134 + <label class="field field-span">
135 + <span>ENS name <em>(optional)</em></span>
136 + <input id="ens_name" name="ens_name" autocomplete="off" maxlength="255" placeholder="treasury.acme.eth" />
137 + </label>
138 + <label class="field field-span">
139 + <span>Network</span>
140 + <select id="eth_network" name="eth_network">
141 + <option value="ethereum" selected>Ethereum (default)</option>
142 + <option value="base">Base</option>
143 + <option value="arbitrum">Arbitrum</option>
144 + <option value="polygon">Polygon</option>
145 + </select>
146 + </label>
147 + </div>
148 + <p class="hint">Only the public address is sent. Never paste a private key or seed phrase into this form.</p>
149 + </section>
150 +
151 <p class="form-error" id="formError" hidden role="alert"></p>
152
153 <div class="actions">
examples/checkout-funding-picker/styles.css
+7 -1
@@ -143,7 +143,7 @@ h3 {
143
144 .method-grid {
145 display: grid;
146 - grid-template-columns: repeat(3, 1fr);
146 + grid-template-columns: repeat(4, 1fr);
147 gap: 0.65rem;
148 }
149
@@ -378,6 +378,12 @@ h3 {
378 margin: 0;
379 }
380
381 +@media (max-width: 720px) {
382 + .method-grid {
383 + grid-template-columns: repeat(2, 1fr);
384 + }
385 +}
386 +
387 @media (max-width: 560px) {
388 .header {
389 flex-direction: column;
guides/checkout-funding-picker.mdx
+4 -3
@@ -1,7 +1,7 @@
1 ---
2 title: "Funding Method Picker Example"
3 sidebarTitle: "Funding Method Picker"
4 -description: "Checkout UI demo for binding USDC, card, or ACH bank_transfer to a ProcureNet v7 session, with optional live POST."
4 +description: "Checkout UI demo for binding USDC, card, ACH bank_transfer, or eth_wallet to a ProcureNet v7 session, with optional live POST."
5 ---
6
7 This repository includes a self-contained funding-method picker under `examples/checkout-funding-picker/`. Use it as a reference when building buyer-facing checkout that calls `POST /api/v7/sessions/{id}/funding-method`.
@@ -13,8 +13,9 @@ This repository includes a self-contained funding-method picker under `examples/
13 | `usdc` | Network select (`base`, `arbitrum`, `polygon`, `ethereum`) | `{ method, network }` |
14 | `card` | Method selection only | `{ method: "card" }` |
15 | `bank_transfer` | Routing number, account number, account type, account holder name | Full ACH fields |
16 +| `eth_wallet` | Wallet address, optional ENS, network (default `ethereum`) | `{ method, wallet_address, network, ens_name? }` |
17
17 -The demo validates ACH fields client-side (9-digit ABA routing with check digit, 4–17 digit account number, `checking` \| `savings`, holder name length), previews the JSON body, and can **POST live** to your API base URL with a Bearer JWT.
18 +The demo validates ACH and ETH fields client-side (ABA check digit; `0x` + 40 hex address), previews the JSON body, and can **POST live** to your API base URL with a Bearer JWT.
19
20 ## Config
21
@@ -33,5 +34,5 @@ Or open `examples/checkout-funding-picker/index.html` directly in a browser.
34
35 ## Related
36
36 -- [POST /funding-method](/api/sessions-funding-method) — full request/response and ACH validation rules
37 +- [POST /funding-method](/api/sessions-funding-method) — full request/response, ACH, and ETH wallet rules
38 - [Integrate Checkout](/guides/integrate-checkout) — end-to-end session flow
guides/integrate-checkout.mdx
+25 -1
@@ -124,7 +124,7 @@ const prefillData = await prefillRes.json();
124
125 ### Set the Funding Method
126
127 -Once the buyer has selected a payment method, register it against the session by calling `POST /api/v7/sessions/{id}/funding-method`. This endpoint requires a Bearer JWT. Supported methods are `usdc` (with `network`), `card`, and `bank_transfer` (ACH).
127 +Once the buyer has selected a payment method, register it against the session by calling `POST /api/v7/sessions/{id}/funding-method`. This endpoint requires a Bearer JWT. Supported methods are `usdc` (with `network`), `card`, `bank_transfer` (ACH), and `eth_wallet` (wallet address + network; optional ENS).
128
129 See [POST /funding-method](/api/sessions-funding-method) for the full field reference. A static funding-method picker demo lives at `examples/checkout-funding-picker/` in the docs repo.
130
@@ -189,6 +189,30 @@ See [POST /funding-method](/api/sessions-funding-method) for the full field refe
189 Always send `routing_number` and `account_number` as strings so leading zeros are preserved. Full account numbers are never returned in responses — only `routing_number_last4` and `account_number_last4`.
190 </Note>
191 </Tab>
192 + <Tab title="ETH wallet (eth_wallet)">
193 + ```typescript
194 + const fundingRes = await fetch(
195 + `https://api.procurenet.io/api/v7/sessions/${session_id}/funding-method`,
196 + {
197 + method: 'POST',
198 + headers: {
199 + 'Content-Type': 'application/json',
200 + 'Authorization': `Bearer ${token}`
201 + },
202 + body: JSON.stringify({
203 + method: 'eth_wallet',
204 + wallet_address: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0',
205 + ens_name: 'treasury.acme.eth', // optional
206 + network: 'ethereum' // ethereum | base | arbitrum | polygon
207 + })
208 + }
209 + );
210 + ```
211 +
212 + <Note>
213 + `wallet_address` must be `0x` + 40 hex characters. Never send private keys. Settlement is authorized in the buyer wallet after the method is bound.
214 + </Note>
215 + </Tab>
216 </Tabs>
217
218 ### Advance Through Session States