Your first payment,
in minutes.
Create a checkout, let your customer choose an asset, and receive a verified payment event. Your prices stay in dollars.
Run your development environment
Clone this workspace and install the dependencies. You’ll need Node.js 24.x and Foundry’s forge, anvil, and cast commands.
npm install
cp .env.example .env.local
# Generate two different secrets for .env.local:
openssl rand -hex 32 # WEBHOOK_MASTER_KEY
openssl rand -hex 32 # WORKER_SECRET
npm run dev
# In another terminal:
npm run chain:local
# In a third terminal:
npm run workerOpen your app at the exact origin configured in APP_URL. Register a merchant account, create a project, and verify your receiving wallet in Settlement. Generate a test API key in Developers.
Use a separate development wallet with Anvil’s public test accounts. Never put real assets in a wallet derived from a public test mnemonic.
Authentication
Keep your sc_sk_test_… key on your server. Send it as a Bearer token. Publishable keys start with sc_pk_test_… and cannot create sessions.
Authorization: Bearer sc_sk_test_...
Content-Type: application/json
Idempotency-Key: order_1042Keys belong to a project and have explicit scopes: checkout:write, checkout:read, payments:read, webhooks:read, and webhooks:write. Secrets are shown only once; create a replacement before revoking a deployed key.
Create a checkout session
The SDK packages are included in this npm workspace. Build them with npm run build:sdk. For another repository, pack each package locally and install the resulting tarball; these packages are not published to the public npm registry.
/v1/checkout/sessionsimport { StockCheckout } from '@stockcheckout/js/server';
const stockcheckout = new StockCheckout(
process.env.STOCKCHECKOUT_SECRET_KEY!,
{ apiBase: process.env.STOCKCHECKOUT_API_BASE! }
);
const session = await stockcheckout.checkout.sessions.create({
amount: 1000, // Exactly $10.00 in USD cents
currency: 'USD',
acceptedAssets: ['USDC', 'NVDA', 'HOOD'],
successUrl: 'https://your-store.com/success',
cancelUrl: 'https://your-store.com/cart',
metadata: { order_id: 'order_1042' },
}, { idempotencyKey: 'order_1042' });
// Redirect your customer to this hosted checkout:
return Response.redirect(session.checkout_url, 303);Calculate prices on your server. Use integer cents and one idempotency key for each logical order. Reusing a key with different parameters returns 409 idempotency_conflict. Sessions expire after 24 hours.
Hosted checkout
Redirect to the session’s checkout_url, or use the React button. It opens the hosted checkout, where your customer reviews the asset amount and authorizes the payment.
import { StockCheckoutButton } from '@stockcheckout/react';
<StockCheckoutButton
sessionId={session.id}
publishableKey="sc_pk_test_..."
stockcheckoutOrigin="https://your-stockcheckout-app.com"
onError={(error) => setError(error.message)}
/>
The success_url receives a session_id query parameter. A redirect is not proof of payment: verify the session on your server or process the signed paid event before fulfilling the order.
Quotes & expiry
A quote binds the exact token amount, merchant wallet, asset, chain, router, session, nonce, and expiry. Amounts use integer arithmetic and round up to the smallest token unit.
/v1/checkout/sessions/:id/quotes{ "asset": "NVDA" }Quotes last at most two minutes and never outlive the session. Expired quotes require a new price and customer review. Token approval is for the exact amount. A payment mined within the signed expiry can still be reconciled after the quote expires.
Receive the paid event
Add your endpoint in Webhooks before taking a payment, and save the signing secret. Verify the original bytes before parsing the body.
import { constructEvent } from '@stockcheckout/js/server';
const rawBody = await request.text();
const signature = request.headers.get('StockCheckout-Signature');
const event = constructEvent(
rawBody,
signature!,
process.env.STOCKCHECKOUT_WEBHOOK_SECRET!
);
if (event.type === 'checkout.session.paid') {
// In one database transaction:
// 1. Deduplicate event.id.
// 2. Retrieve the authenticated session.
// 3. Match the order, amount, currency and paid status.
// 4. Record fulfillment and the processed event ID.
}
return new Response('ok', { status: 200 });The SDK checks the HMAC signature in constant time and rejects timestamps outside a five-minute window. Your application must persist processed event IDs to reject duplicate deliveries.
StockCheckout uses a durable outbox, database leases, response logging, and automatic retries. Run the worker regularly. The complete merchant example under examples/merchant implements durable order fulfillment and event deduplication.
| Event | Meaning |
|---|---|
checkout.session.paid | Settlement verified and recorded. Safe to begin fulfillment after matching your order. |
| Delivery retries | 60 seconds, 5 minutes, 30 minutes, 2 hours, 12 hours, 24 hours, and 48 hours. |
Payments & receipts
Only the backend can mark a checkout paid. It checks the canonical successful receipt, transaction calldata and signature, router, session, merchant, token, raw amount, and confirmation depth. Each payment creates an immutable accounting record exactly once.
const session = await stockcheckout.checkout.sessions.retrieve(sessionId);
if (session.status === 'paid') {
// Match session.metadata.order_id and session.amount.
}
const { payments } = await stockcheckout.payments.list();
const { payment } = await stockcheckout.payments.retrieve(paymentId);Open /receipt/:sessionId for a verified receipt. Export your payment records from the dashboard. A worker scans the router’s confirmed events so a payment can be recovered even if the customer closes the browser immediately after broadcasting.
Refunds
Settlement is direct to the merchant. A refund requires the merchant to sign and send a separate transaction. This release does not implement automated refund verification or partial refunds, and never reports an unverified refund as completed.
Test mode
The local chain uses ID 31337 and RPC http://127.0.0.1:18544. The managed setup deploys the router and funds the first three public Anvil accounts with Test USDC, Test NVDA, and Test HOOD.
# Public local-development keys only.
# Merchant: account 1. Customer: account 2.
cast wallet private-key \
"test test test test test test test test test test test junk" 1
cast wallet private-key \
"test test test test test test test test test test test junk" 2Test rejection by canceling a wallet request. Test an expired quote by waiting two minutes, and insufficient balance by selecting an unfunded wallet. Test webhook retry by returning HTTP 500, then restoring a 2xx response and retrying the delivery.
Errors
Errors return a non-2xx HTTP status and an object containing error.code and a clear error.message. The server SDK throws StockCheckoutError with the status, code, and request ID.
{
"error": {
"code": "idempotency_conflict",
"message": "Idempotency key was already used with a different request."
}
}| Code | Next step |
|---|---|
wallet_required | Verify your receiving wallet first. |
asset_unavailable | Choose a supported asset or check chain setup. |
session_expired | Create a new checkout. |
idempotency_conflict | Reuse the original request or create a new order key. |
insufficient_scope | Use a key with the required scope. |
origin_mismatch | Open the exact origin configured in APP_URL. |
rate_limited | Wait for the limit window before retrying. |
SDK reference
@stockcheckout/js/server
Server session creation and retrieval, payments, endpoints, and webhook verification.
@stockcheckout/js
Browser-safe hosted checkout redirect. Accepts a publishable key.
@stockcheckout/react
StockCheckoutButton with styling, error handling, and optional cancellation.
Source, type declarations, complete setup instructions, security boundaries, and operations guidance ship in this repository. Build with npm run build:sdk; run npm test to validate the SDK and backend.