Skip to content

Session Payment

Session Payment is the primary way to accept payments through Stablecoin Harness. A session is an invoice: the customer opens a payment link, picks a stablecoin and chain, and sends the exact amount to a deterministic deposit address unique to that session and chain.

Customer wallet ─► Deposit address ─► swap → sUSDe ─► Merchant wallet
(CREATE2 Forwarder) (on receipt) (settlement)
  1. Your server creates a session and gets back a hosted paymentUrl.
  2. Each (session, chain) gets a unique deposit address — a CREATE2-predicted Forwarder clone.
  3. The customer sends a supported stablecoin (USDC / USDT / USDe) to that address.
  4. On detection, the backend deploys the Forwarder clone and, in one transaction, routes the inputs through a whitelisted swap aggregator into sUSDe, delivered straight to your merchant wallet (your ERC-4337 smart account).
  5. A signed session.confirming webhook fires; if you supplied successUrl, the checkout page redirects the payer there once the webhook is delivered.

For L2 chains the funds are swapped to USDe and bridged to a mainnet-path chain via LayerZero V2, where VaultComposerSync stakes into sUSDe for the merchant in the same delivery — surfaced as the bridging status. See L2 Bridge & Vault Compose.

Terminal window
curl -X POST https://api.harness.stablecoinx.com/v1/sessions \
-H "Authorization: Bearer $STABLECOINX_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": "49.99",
"merchantName": "Acme Coffee",
"lineItems": [
{ "name": "Latte (x2)", "quantity": 2, "unitPrice": "5.00", "total": "10.00" },
{ "name": "Beans 1kg", "quantity": 1, "unitPrice": "39.99", "total": "39.99" }
],
"paymentOptionSymbols": ["USDC", "USDT", "USDe"],
"expiresInSec": 1800,
"successUrl": "https://example.com/orders/1234/complete"
}'

Request fields: amount (USD decimal string, required); merchantName?; lineItems?; paymentOptionSymbols? (subset of ["USDC","USDT","USDe"], default all three); expiresInSec? (60 s – 7 days, default 3600); successUrl? (absolute https://); sandboxMode? (boolean — testnet-only when true). There is no currency field and no top-level chain — chains come from the payment options.

Response:

{
"id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"paymentUrl": "https://pay.stablecoinx.com/session/a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"status": "awaiting_payment",
"expiresAt": "2026-05-26T17:30:00Z",
"createdAt": "2026-05-26T17:00:00Z"
}

Send the customer the paymentUrl. To read full details — line items, and the per-chain deposit addresses inside paymentOptions[] — call GET /v1/sessions/:id.

pending → awaiting_payment → confirming → bridging → completed
↘ expired ↘ failed ↘ failed
StatusMeaning
pendingCreated, not yet open for payment. Usually transient.
awaiting_paymentOpen. Deposit addresses are live.
confirmingOn-chain deposit detected; routing through the forwarder + swap. session.confirming webhook fires here.
bridging(L2 only) final sUSDe being bridged to mainnet.
completedFunds delivered to your wallet as sUSDe. Terminal.
failedOn-chain execution reverted (slippage, etc.). Terminal.
expiredPassed expiresAt without a deposit. Terminal.

No background job actively flips sessions to expired today — the deposit monitor ignores past-expiry sessions, so check expiresAt against the current time on awaiting_payment rows.

Today the API emits a single event type, session.confirming, fired when a session leaves awaiting_payment. Register endpoints from the dashboard or via the webhook management API. Each delivery is signed — see Quickstart → Verify webhook signatures.

Sample delivery:

{
"id": "evt_550e8400-e29b-41d4-a716-446655440000",
"type": "session.confirming",
"createdAt": "2026-05-26T17:05:23Z",
"data": {
"session": {
"id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"merchantName": "Acme Coffee",
"amount": "49.99",
"status": "confirming",
"expiresAt": "2026-05-26T17:30:00Z",
"createdAt": "2026-05-26T17:00:00Z",
"paidAt": "2026-05-26T17:04:48Z",
"txHash": "0xcdf4221a..."
}
}
}

Manage endpoints with an sk_live_* key:

MethodPathPurpose
POST/v1/webhooksRegister an endpoint (returns the signing secret once, whsec_*).
GET/v1/webhooks · /v1/webhooks/:idList / inspect.
PATCH/v1/webhooks/:idUpdate url / events / enabled.
DELETE/v1/webhooks/:idDelete (cascades delivery history).
POST/v1/webhooks/:id/rotate-secretIssue a fresh signing secret.
POST/v1/sessions/:id/retry-webhookRe-dispatch the most recent event for a session.

Up to 5 endpoints per merchant. Failed deliveries retry on backoff 1m → 5m → 30m → 2h (5 attempts).

Deposit addresses are deterministic — a CREATE2-predicted Forwarder clone per (session, chain), derived from the session UUID and chain ID. The contract is not deployed until funds arrive (ERC-20 balances accumulate at the predicted address). Read the resolved addresses from paymentOptions[].depositAddress on GET /v1/sessions/:id; options on chains without a deployed Forwarder factory are filtered out server-side.

ChainSettlement asset
BasesUSDe
ArbitrumsUSDe
EthereumsUSDe

Testnets (Base Sepolia, Arbitrum Sepolia, Ethereum Sepolia) are reachable via sandboxMode: true — same code path, testnet stablecoins.

Paymaster

Sponsor or accept gas in stablecoins for the customer’s transaction. Read →

Treasury Dashboard

Operate the funds that settle from sessions. Read →